Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.47.0-wmf.23 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 Introduire la biodiversité dans la construction et l'urbanisme/Où, quand et comment intégrer la biodiversité dans et autour du bâti ? 0 40134 775042 374219 2026-10-12T13:05:07Z Snawei 81149 ortho 775042 wikitext text/x-wiki ==Où, quand et comment intégrer la biodiversité dans et autour du bâti ?== [[File:Terrasse végétalisée MNE 03.jpg|thumb|350px|left|Il est utile de pouvoir tester différents types de plantes et de configurations, car la température, la luminosité, l'[[w:hygrométrie|hygrométrie]], la pollution de l'air peuvent sélectionner certaines plantes]] [[File:Boutiquehotel Stadthalle Wien.jpg|thumb|350px|Dans les cas où le bâti a des fonctions d'habitation, de bureau, d'atelier, etc ou quand il supporte des panneaux solaires, des éléments fragiles ou vulnérables à la faune ou à la flore.. un équilibre est à trouver entre les surfaces à végétaliser ou à couvrir de panneaux solaires, la flore ne devant par exemple ici ni couvrir ni ombrer les cellules photovoltaïques]] [[File:Grünes Haus Gießen.jpg|thumb|350px|Certaines plantes comme la [[w:Vigne vierge|vigne vierge]] poussent rapidement, et en s'accrochant avec des crampons qui laissent des traces. Il est recommander de les contrôler par une taille suffisamment fréquente, de manière à éviter comme ici qu'elles ne couvrent les cheminées, la toiture (sauf si elle a été conçue pour) et les chéneaux qui risquent de se boucher. Cette végétation caduque de zone tempérée est intéressante sur les façades sud (rafraichies par les feuille en été, et pouvant se réchauffer au soleil en hiver)]] La réponse à cette question varie selon * le type de bâti (architectonique), * les fonctions dévolues au bâti, * les matériaux, * le contexte [[w:fr:Bogéographie|biogéographique]] * et d'autres éventuels éléments de contextes (par exemple on veillera à éviter les plantes toxiques sur les murs et terrasses d'une école maternelle). <br />L'essentiel est que la question soit posée * bien en amont du projet, * en tenant compte du contexte biogéographique actuel et à venir (certaines régions sont plus à risque en terme de risque climatique * en tenant compte de l'[[w:fr:écopotentialitéécopotentialité]] du site et des environs * avec une approche prospective des usages futurs du bâti et du devenir des végétaux (ex : ombres portées des arbres) ==Exemple de questions à se poser en amont== * Quel est le poids de végétation que peuvent supporter les différentes parties du bâti ? * Y a t il un substrat convenable et en suffisance, et assez humide pour les plantes grimpantes ou suspendues ? * les matériaux utilisés sont-ils tous éco-compatibles ? * l'entretien de la flore (grimpante, suspendue, etc.) sera-t-il aisé ou assez facile * le bâti abritera-t-il des animaux domestiques tels que chiens et chats (si oui, il convient de ne pas disposer de nichoirs à oiseaux ou chauve-souris à leur portée, et anticiper sur le fait que les chats sauront escalader la façade si elle est couverte d'une vigne vierge ou d'un lierre assez ancien) * les fenêtres et ouvertures seront elles ou pourront elles être dotées de moustiquaires les systèmes d'aération/ventilation seront ils bien munis de grilles interdisant l'intrusion de la petite faune et des insectes. * les gouttières ou chéneaux sont ils accessibles et faciles à entretenir ou protéger des plantes grimpantes (qui ne doivent pas les boucher) * quels risques particuliers peuvent se poser pour des enfants, personnes âgées, handicapés ?. comment les limiter ? (ex : entourer une mare d'une clôture, etc.) * ou positionner et comment protéger les nichoirs susceptibles d'abriter des espèces protégées ou vulnérables, une ruche, etc. {{...}} [[Catégorie:Introduire la biodiversité dans la construction et l'urbanisme (livre)]] fbli2wr4q8yaamn64vua8y45gzbvo4f Essai de prospective environnementale. 2040, nord de la France... 0 45969 775073 724990 2026-10-12T18:36:21Z Snawei 81149 ortho 775073 wikitext text/x-wiki {{NavDébut|book={{PAGENAME}}|page=Avant-propos|pageText=Continuer}} __NOTOC__ __NOEDITSECTION__ <div style="background: #EAF5FB; font-size: 120%; margin: 1em auto; padding: 0.3em; text-align: center; width: 70%;"> <div style="background: #EAF5FB; border: 2px solid #fff; padding: 0.3em;"> ''Ce livre appartient aux séries [[Architecture]] et [[Écologie]]. Il est actuellement rangé sur l'[[Accueil/Architecture|étagère Architecture]] de la bibliothèque [[Accueil|Wikilivres]]''. </div> </div> <div style="text-align: center;font-size:300%; line-height: 0.8em; padding-top: 0.5em; padding-bottom: 0.25em;"> Essai de prospective environnementale. <br /><br />2040, nord de la France </div> {| width="100%" align="center" border="0" cellpadding="4" cellspacing="4" |- | valign="top" width="40%" style="border: 1px solid #CCCC99; background-color: #F4EEA9"| [[Image:990x608-Carte-Région-Nord-Pas-de-Calais-R.jpg|thumb|150px|right|Carte actuelle de la [[w:Nord-Pas-de-Calais|région Nord-Pas-de-Calais]]. <br /><br />Que sera-t-elle en [[w:2040|2040]] ? [[w:2050|2050]] ? [[w:2100|2100]] ? (trois dates butoir pour les grandes conventions internationales sur le Climat ou la Biodiversité)]] [[File:GreenWallMurVégétal.jpg|thumb|150px|right|Mur végétal intérieur ([[w:Lille|Lille]], France)]] '''Avant-propos''' '''Par qui et pour qui est fait ce livre ? ''' Cet '''''« atelier ouvert d'écriture collaborative et prospective »'''''' a été informellement lancé (en 2011) à l'initiative d'un groupe de prospectivistes, sur la base d'une trame écrite en Janvier 2010 par et pour des citoyens et personnes de différents métiers, intéressés par la prospective environnementale ([[w:Architecte|architecte]]s, [[w:Architecte|urbaniste]]s, [[w:Ingénieur|ingénieur]]s, [[w:aménagement du territoire|aménageur]]s, [[w:étudiants|étudiant]]s, [[w:écologue|écologue]]s, [[w:sociologue|sociologue]]s, [[w:Système d'information géographique|sigiste]]s, et d'autres professionnels de [[w:Collectivité territoriale|collectivité]]s, des secteurs publics ou privé intéressés par leur avenir, celui de leurs enfants, des [[w:Générations futures|générations futures]] et de la planète et des sociétés qui l'habitent et l'habiteront... Il est aussi fait par et pour des citoyens soucieux d'intégrer les principes et critères du [[w:Développement durable|développement '''''« durable »''''']] et ''« soutenable »'' dans l'aménagement du territoire et la préparation de leur futur. La date de [[w:2040|2040]] a été choisie par ce que « ''date butoir ''» pour certains documents d'[[w:fr:Aménagement du territoire|aménagement du territoire]] et de gouvernance mondiale. <br/>Tous et chacun, selon ses compétences et plaisirs peuvent s'associer à ce projet de livre, par exemple en tant que relecteurs, illustrateurs, co-animateurs, analystes ou contributeurs (dans le texte ou sur la [[Discussion:Essai de prospective environnementale. 2040, nord de la France...|page de discussion]]). Chacun peut aussi simplement profiter du contenu qui lui est offert et rêver ou participer aux avenirs possibles. C'est notamment un support de travail possible pour des élèves, enseignants ou étudiants. <br />La rédaction de ce livre a commencé en Août 2009 comme exercice individuel, et s'est poursuivie comme exercice collectif et collaboratif à partir de juillet 2011. <div class="noprint" style="text-align:right;margin-right:10px;margin-bottom:4px;">[[Image:Wikipedia-logo.png|16px]] '''[[w:Prospective|En savoir plus sur la prospective...]]''' </div> <div class="noprint" style="text-align:right;margin-right:10px;margin-bottom:4px;">[[Image:Wikipedia-logo.png|16px]] '''[[w:Aménagement du territoire|En savoir plus sur l'Aménagement du territoire ...]]''' </div> <div class="noprint" style="text-align:right;margin-right:10px;margin-bottom:4px;">[[Image:Wikipedia-logo.png|16px]] '''[[w:Travail collaboratif|En savoir plus sur le travail collaboratif...]]''' </div> | valign="top" width="60%" style="border: 1px solid #CCCC99; background-color: #F4EEA1"| '''Pourquoi ce livre ?''' '''Les parti-pris de départ (contraintes stimulantes de l'exercice).''' * L'histoire commence en 2040 (mais quelques ''flash-back'' sont possibles) ; Et le récit est géographiquement centrée sur le nord de la France, même si certains éléments ou épisodes, pour en éclairer le contexte se déroulent dans d'autres lieux ; * Comme un roman ou une histoire de [[w:science-fiction|science-fiction]], cet essai de prospective doit être agréable à lire (l'humour, l'imagination et le suspense n'y sont pas interdits, au contraire, tant qu'ils servent le fond qui est la prospective environnementale ; * les évènements qui s'y produisent doivent être scientifiquement, économiquement, sociologiquement et environnementalement crédibles (les justifications des évènements qui émailleront le cours du récit seront peu à peu expliqués, discutés, sourcés, de manière neutre (au sens wikipédien du terme) en note de bas de page, ou en page de discussion si cela nécessite plus de place) ; * il ne s'agit pas d'un pamphlet ni d'un moyen de régler des comptes. On évitera en particulier de citer des noms de personnes existantes, ainsi que toute contribution diffamatoire ou gratuitement agressives). Chaque contributeur sera responsable de ses ajouts ou retraits et pourra être amené à les argumenter. * Le travail est collaboratif ; c'est aussi à son échelle un exercice de démocratie participative. * Le contenu doit illustrer des questions et enjeux de prospective environnementale, lesquels seront à justifier en page de discussion s'il advenait qu'ils ne sont pas compris comme tels par certains lecteurs ou contributeurs apportant leurs arguments). Cette contrainte fait que le contenu traitera principalement des questions d'environnement et de climat (deux thèmes mis en avant depuis 1992 comme enjeux majeurs, locaux et planétaires par l'ONU, et un nombre croissant d'échelles de gouvernance. Le climat et l'environnement sont les vrais sujets de l'histoire, même si une intrigue impliquant un certain nombre de personnages, dont un grand père et sa petite-fille en est le fil conducteur. * Il ne s'agit pas ici de prédire l'avenir (qui le pourrait ? et ce n'est pas l'objet de la [[w:Prospective|prospective]]), mais de rassembler dans une histoire des morceaux de futurs possibles, désirables ou non, pour y faire réfléchir, pour les rêver ou les étudier plus méthodiquement (dans les pages de discussion par exemple, ou dans d'autres lieux où l'avenir peut s'imaginer). Pour des raisons d'intérêt narratif, certains faits pourront être grossis, rendus dramatiques, ou rapprochés dans le temps ou l'espace plus que les lois de la probabilité n'y inviteraient à le faire, mais ils doivent toujours rester individuellement crédibles et explicables par des sources publiées (Nous proposons de reprendre ici l'un des principes de Wikipédia). Si un évènement de l'histoire a été exagéré pour des raisons narratives, les auteurs sont invités à le préciser en page de discussion ou en note de bas de page. * Des versions alternatives ou concernant d'autres régions pourront si certains le jugent nécessaire ou utile faire l'objet d'autres wikibooks ou travaux. * Un objectif transversal est - une fois de recul pris par le lecteur - d'être un document pédagogique. Il ne s'agit en aucun cas d'une prédiction ou d'une écriture de l'avenir, mais bien d'une histoire pour y réfléchir individuellement et collectivement. * Enfin, ceci est une expérience et aventure pour les auteurs et autres contributeurs ; une première expérience de ce genre à notre connaissance, permise par les outils de type wiki... Peut être une main que nous tend l'avenir. Les contributeurs sont invités à scientifiquement et/ou techniquement expliquer, valider et sourcer les contenus des ajouts ou retraits lorsqu'ils modifient fortement, tout en restant courtois et en respectant les contributeurs précédents. <div align="right" style="font-size:xx-small;">[{{SERVER}}{{localurl:Modèle:Livre de cuisine/Lumière sur|action=edit}} Modifier]</div> |} {| width="99%" align="center" border="0" cellpadding="4" cellspacing="4" style="border: 1px solid #CCCC99; background-color: #F4EEA1" |<div align="left"> '''Pré-requis''' Des connaissances de bases (ou élaborées pour les chapitres les plus techniques) en architecture et [[w:Aménagement du territoire|aménagement]] sont recommandées, ainsi qu'en matière d'[[w:écologie du paysage|écologie du paysage]], [[w:urbanisme|urbanisme]], [[w:architecture|architecture]], [[w:santé|santé]], [[w:sociologie|sociologie]], [[w:démographie|démographie]], etc. <br />Les auteurs chercheront à partager leurs connaissances de la manière la plus claire possible, afin que tous et chacun puissent en bénéficier. <br />Un glossaire, des schémas et illustrations, ainsi que des liens bibliographiques et des liens vers des articles de [[w:Wikipédia|wikipédia]] ou des sites externes aideront le lecteur qui veut en savoir plus. </div> <div align="right" style="font-size:xx-small;">[{{SERVER}}{{localurl:Modèle:Recette du mois|action=edit}} Modifier]</div> |} {| width="99%" align="center" border="0" cellpadding="4" cellspacing="4" style="border: 1px solid #CCCC99; background-color: #F4EEA1" | '''Comment participer et collaborer à la rédaction de ce livre ?''' <br />Chacun peut contribuer à ce projet, par exemple ; * comme rédacteur; * comme relecteur/correcteur (''pour faciliter le travail de toute l'équipe, ne pas oublier de remplir la case "résumé" en justifiant les corrections ou modifications de mise en page''); * comme ''« discutant »'' dans la [[Discussion:Essai de prospective environnementale. 2040, nord de la France...|page de discussion]](il en existe une pour le livre et une par chapitre et page); * comme illustrateur, photographe, graphiste ou pour la mise en page... * comme coordinateur du travail fait ou à faire autour d'un chapitre, d'un thème particulier que vous connaissez bien Ce livre, en cours de construction, est un [[W:Travail collaboratif|projet collaboratif]]. Si vous y trouvez des erreurs, corrigez les directement à l'aide des liens « ''Modifier'' » sur chaque page, ou poser des questions et apportez vos remarques et suggestions en laissant un [[Discussion:Essai de prospective environnementale. 2040, nord de la France...|nouveau message]] sur la page de discussion de ce livre. Toute question ou participation est bienvenue ! Dans tous les cas : * Il est recommandé aux contributeurs de commencer par se créer un compte ([[Wikilivres:Dialogue entre contributeurs|pour dialoguer avec les autre contributeurs]]) puis à utiliser la page de discussion du projet ; * Merci d'utiliser un [[Wikilivres:La neutralité de point de vue|point de vue neutre]], de toujours utiliser des [[Wikilivres:Ressources libres de droit|ressources libres de droit]] et dans la mesure du possible, comme sur Wikipédia, [[w:Wikipédia:Citez vos sources|citez|vos sources]], et dans la mesure du possible avec un objectif de [[w:Wikipédia:Vérifiabilité|vérifiabilité]], pour améliorer ou compléter votre travail conformément à la [[Wikilivres:Charte pédagogique|charte pédagogique]] de Wikilivre. Merci de ne pas insérer de messages publicitaires ou personnels dans ce livres. Ils en seraient retirés. * Si sous débutez sur les wiki, voir la [[Aide:Syntaxe#Identification dans les pages de discussion|page d'aide]] pour la syntaxe. * Il est souvent plus facile (et plus écologique, chaque aller-et retour et travail d'un serveur informatique est consommateur d'énergie et de bande passante) de préalablement préparer un ajout, ou un élément de discussion sur votre éditeur favori (Word par exemple) en prenant le temps de le compléter ou de vérifier les erreurs typographiques ou grammaticales plus aisément; puis de recopier le tout via votre navigateur, pour le publier sur le wikibook ou sa page de discussion, après une dernière prévisualisation. Vous pouvez ainsi garder votre propre version de sauvegarde, ou au moins une version vous permettant de travailler hors-ligne. * Quand plusieurs personnes travaillent en même temps à modifier une même page, des "conflits d'édition sont possibles". Il est alors recommandé de prévenir les autres contributeurs qu'on effectue une modification importante, ou de travailler par petites touches rapides. * Certains contributeurs peuvent apprécier travailler en sous-groupes sur un thème particulier. Ne pas hésiter à le préciser dans les pages de discussions. <div align="right" style="font-size:xx-small;">[{{SERVER}}{{localurl:Modèle:Recette du mois|action=edit}} Modifier]</div> |} ---- <div style="font-size:250%; line-height: 0.8em; padding-top: 0.5em; padding-bottom: 0.25em;"> Sommaire provisoire <br /> Index complet des chapitres et sous-chapitres </div></div> #'''[[/Avant-propos/]]'''{{25}} #'''[[/Où l'on découvre l'auteur, Papyboom, en 2040/]]''' #'''[[/Souvenirs des années 2020/]]''' #'''[[/Tranche de vie, 2040/]]''' #'''[[/Croque la vie/]]''' #'''[[/À l'ombre des forêts/]]''' #'''[[/L'ombreur solaire/]]''' #'''[[/Annexes/]]'''{{25}} ##[[/Annexes/Bibliographie|Bibliographie]]{{25}} ##[[/Annexes/Glossaire|Glossaire]]{{25}} ##[[/Annexes/Autres ressources|Autres ressources]]{{00}} ---- {{CDU|7/72/720|720.0/720.0.0}} {{CDU|3/37/372|372.8/372.89.0}} ---- [[Catégorie:Aménagement du territoire]] [[Catégorie:Livres en cours de rédaction]] {{AutoCat}} 4a4gwevdk5plelsr6xgg4hz7dgkrzni Fonctionnement d'un ordinateur/Langage machine et assembleur 0 65776 775087 774326 2026-10-12T20:56:23Z Mewtow 31375 /* Les processeurs avec des instructions flottantes dédiées */ 775087 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 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 registres flottants=== 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. Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant). ==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== Les '''méta-instructions''' 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 de calcul d'adresse== Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons. ===Les tableaux : des données consécutives en RAM=== Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux. Les '''tableaux''' regroupent plusieurs données de même type, qui 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''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé. Toute donnée dans un 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. Dans ce qui suit, nous supposons que cet indice est dans un registre. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base 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> La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée. ====Le calcul base + indice décalé==== Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent. En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets. Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type : : <math>\text{Adresse de base} + \text{Indice} << n</math> Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler). Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés. Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé. ====Le calcul base + indice décalé + décalage==== Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme : : <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math> L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça. Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons). Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante. ===L'adressage relatif et le code indépendant de la position=== [[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]] Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions : * une qui mémorise le '''code du programme''', à savoir ses instructions ; * une qui mémorise des '''données statiques''', qui sont toujours au même endroit ; * et une autre pour des '''données variables'''. Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution. A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel. Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ? Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ? Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''. Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première. La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM. L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres. Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus. Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate. ==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> enz1ie4d6finy3sqcikjkfzwzvzdtnb 775088 775087 2026-10-12T21:04:53Z Mewtow 31375 /* L'instruction SKIP */ 775088 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 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 registres flottants=== 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. Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant). ==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 instructions de comparaison flottantes=== Plus haut, nous avons vu que les instructions flottantes sont séparées des instructions entières. Non seulement, les instructions de calcul flottantes sont séparées, mais les instructions de lecture/écriture le sont aussi. Et il en est de même pour les comparaisons. Il y a donc des opérations de '''comparaison flottante''', et même des '''branchements flottants'''. Il est possible d'utiliser une instruction de test/branchement unique pour les entiers et flottants, mais c'est la solution minoritaire. La solution la plus fréquent utilise des instructions de test et de branchement séparées pour entiers et flottants. Il y a plusieurs raisons à cela, et on retrouve le fait que les registres flottants sont séparés. De plus, comparer deux flottants n'est pas la même chose que comparer deux entiers. Il faut utiliser une unité de calcul dédiée pour comparer deux flottants, qui est différente d'un comparateur entier. Un comparateur entier est généralement un circuit soustracteur camouflé, qui est souvent fusionné avec l'ALU entière. Mais le comparateur flottant est lui plus complexe et demande de faire des normalisations, de tester des cas spéciaux, doit gérer les dénormaux, etc. Il a souvent un circuit dédié dans la FPU. Mais une autre raison, beaucoup plus fondamentale, est que le registre d'état est séparé entre les opérations entières et les opérations flottantes. Le registre d'état flottant est foncièrement différent du registre d'état entier, il n'a pas les mêmes bits. Par exemple, il a un bit qui indique qu'un résultat est un flottant dénormal. Il n'a de sens que pour le registre d'état flottant, n'a pas d'équivalent dans le registre d'état entier, et il faut des instructions de test dédiées pour le tester. ==Les méta-instructions REPEAT et EXECUTE== Les '''méta-instructions''' 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 de calcul d'adresse== Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons. ===Les tableaux : des données consécutives en RAM=== Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux. Les '''tableaux''' regroupent plusieurs données de même type, qui 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''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé. Toute donnée dans un 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. Dans ce qui suit, nous supposons que cet indice est dans un registre. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base 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> La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée. ====Le calcul base + indice décalé==== Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent. En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets. Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type : : <math>\text{Adresse de base} + \text{Indice} << n</math> Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler). Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés. Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé. ====Le calcul base + indice décalé + décalage==== Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme : : <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math> L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça. Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons). Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante. ===L'adressage relatif et le code indépendant de la position=== [[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]] Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions : * une qui mémorise le '''code du programme''', à savoir ses instructions ; * une qui mémorise des '''données statiques''', qui sont toujours au même endroit ; * et une autre pour des '''données variables'''. Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution. A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel. Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ? Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ? Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''. Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première. La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM. L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres. Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus. Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate. ==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> hws509t060yz5y5sjhpoqgygah7302d 775089 775088 2026-10-12T21:06:47Z Mewtow 31375 /* Les instructions de comparaison flottantes */ 775089 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 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 registres flottants=== 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. Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant). ==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 instructions de comparaison flottantes=== Plus haut, nous avons vu que les instructions flottantes sont séparées des instructions entières. Non seulement, les instructions de calcul flottantes sont séparées, mais les instructions de lecture/écriture le sont aussi. Et il en est de même pour les comparaisons. Il y a donc des opérations de '''comparaison flottante''', et même des '''branchements flottants'''. Il est possible d'utiliser une instruction de test/branchement unique pour les entiers et flottants, mais c'est la solution minoritaire. La solution la plus fréquent utilise des instructions de test et de branchement séparées pour entiers et flottants. Il y a plusieurs raisons à cela, et on retrouve le fait que les registres flottants sont séparés. De plus, comparer deux flottants n'est pas la même chose que comparer deux entiers. Il faut utiliser une unité de calcul dédiée pour comparer deux flottants, qui est différente d'un comparateur entier. Un comparateur entier est généralement un circuit soustracteur camouflé, qui est souvent fusionné avec l'ALU entière. Le comparateur flottant est lui plus complexe : il teste les bits de signe, doit tester des cas spéciaux, les NaNs, les infinis, les dénormaux, etc. Il a souvent un circuit dédié dans la FPU. Mais une autre raison, beaucoup plus fondamentale, est que le registre d'état est séparé entre les opérations entières et les opérations flottantes. Le registre d'état flottant est foncièrement différent du registre d'état entier, il n'a pas les mêmes bits. Par exemple, il a un bit qui indique qu'un résultat est un flottant dénormal. Il n'a de sens que pour le registre d'état flottant, n'a pas d'équivalent dans le registre d'état entier, et il faut des instructions de test dédiées pour le tester. ==Les méta-instructions REPEAT et EXECUTE== Les '''méta-instructions''' 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 de calcul d'adresse== Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons. ===Les tableaux : des données consécutives en RAM=== Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux. Les '''tableaux''' regroupent plusieurs données de même type, qui 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''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé. Toute donnée dans un 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. Dans ce qui suit, nous supposons que cet indice est dans un registre. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base 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> La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée. ====Le calcul base + indice décalé==== Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent. En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets. Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type : : <math>\text{Adresse de base} + \text{Indice} << n</math> Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler). Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés. Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé. ====Le calcul base + indice décalé + décalage==== Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme : : <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math> L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça. Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons). Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante. ===L'adressage relatif et le code indépendant de la position=== [[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]] Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions : * une qui mémorise le '''code du programme''', à savoir ses instructions ; * une qui mémorise des '''données statiques''', qui sont toujours au même endroit ; * et une autre pour des '''données variables'''. Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution. A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel. Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ? Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ? Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''. Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première. La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM. L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres. Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus. Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate. ==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> qs35o3ssv3ol35lolroehkzgi9gq5fj 775090 775089 2026-10-12T21:07:33Z Mewtow 31375 /* Les instructions de comparaison flottantes */ 775090 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 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 registres flottants=== 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. Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant). ==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 instructions de comparaison flottantes=== Plus haut, nous avons vu que les instructions flottantes sont séparées des instructions entières. Non seulement, les instructions de calcul flottantes sont séparées, mais les instructions de lecture/écriture le sont aussi. Et il en est de même pour les comparaisons. Il y a donc des opérations de '''comparaison flottante''', et même des '''branchements flottants'''. Il est possible d'utiliser une instruction de test/branchement unique pour les entiers et flottants, mais c'est la solution minoritaire. La solution la plus fréquent utilise des instructions de test et de branchement séparées pour entiers et flottants. Il y a plusieurs raisons à cela, et on retrouve le fait que les registres flottants sont séparés. De plus, comparer deux flottants n'est pas la même chose que comparer deux entiers. Il faut utiliser une unité de calcul dédiée pour comparer deux flottants, qui est différente d'un comparateur entier. Un comparateur entier est généralement un circuit soustracteur camouflé, qui est souvent fusionné avec l'ALU entière. Le comparateur flottant est lui plus complexe : il teste les bits de signe, doit tester des cas spéciaux, les NaNs, les infinis, les dénormaux, etc. Il a souvent un circuit dédié dans la FPU. Enfin, il y a une dernière raison, beaucoup plus fondamentale, bien que reliée à la précédente. Le registre d'état n'est pas le même les opérations entières et les opérations flottantes. Le '''registre d'état flottant''' est foncièrement différent du registre d'état entier, il n'a pas les mêmes bits. Par exemple, il a un bit qui indique qu'un résultat est un flottant dénormal. Il n'a de sens que pour le registre d'état flottant, n'a pas d'équivalent dans le registre d'état entier, et il faut des instructions de test dédiées pour le tester. ==Les méta-instructions REPEAT et EXECUTE== Les '''méta-instructions''' 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 de calcul d'adresse== Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons. ===Les tableaux : des données consécutives en RAM=== Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux. Les '''tableaux''' regroupent plusieurs données de même type, qui 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''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé. Toute donnée dans un 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. Dans ce qui suit, nous supposons que cet indice est dans un registre. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base 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> La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée. ====Le calcul base + indice décalé==== Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent. En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets. Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type : : <math>\text{Adresse de base} + \text{Indice} << n</math> Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler). Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés. Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé. ====Le calcul base + indice décalé + décalage==== Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme : : <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math> L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça. Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons). Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante. ===L'adressage relatif et le code indépendant de la position=== [[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]] Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions : * une qui mémorise le '''code du programme''', à savoir ses instructions ; * une qui mémorise des '''données statiques''', qui sont toujours au même endroit ; * et une autre pour des '''données variables'''. Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution. A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel. Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ? Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ? Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''. Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première. La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM. L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres. Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus. Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate. ==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> ox0kijrvlz2crzi142ox1ewg9pph7ih 775091 775090 2026-10-12T21:08:40Z Mewtow 31375 /* Les instructions de test qui sont des soustractions déguisées */ 775091 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 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 registres flottants=== 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. Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant). ==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 signé/non-signé permettent de faire la différence entre infériorité stricte ou non. [[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 instructions de comparaison flottantes=== Plus haut, nous avons vu que les instructions flottantes sont séparées des instructions entières. Non seulement, les instructions de calcul flottantes sont séparées, mais les instructions de lecture/écriture le sont aussi. Et il en est de même pour les comparaisons. Il y a donc des opérations de '''comparaison flottante''', et même des '''branchements flottants'''. Il est possible d'utiliser une instruction de test/branchement unique pour les entiers et flottants, mais c'est la solution minoritaire. La solution la plus fréquent utilise des instructions de test et de branchement séparées pour entiers et flottants. Il y a plusieurs raisons à cela, et on retrouve le fait que les registres flottants sont séparés. De plus, comparer deux flottants n'est pas la même chose que comparer deux entiers. Il faut utiliser une unité de calcul dédiée pour comparer deux flottants, qui est différente d'un comparateur entier. Un comparateur entier est généralement un circuit soustracteur camouflé, qui est souvent fusionné avec l'ALU entière. Le comparateur flottant est lui plus complexe : il teste les bits de signe, doit tester des cas spéciaux, les NaNs, les infinis, les dénormaux, etc. Il a souvent un circuit dédié dans la FPU. Enfin, il y a une dernière raison, beaucoup plus fondamentale, bien que reliée à la précédente. Le registre d'état n'est pas le même les opérations entières et les opérations flottantes. Le '''registre d'état flottant''' est foncièrement différent du registre d'état entier, il n'a pas les mêmes bits. Par exemple, il a un bit qui indique qu'un résultat est un flottant dénormal. Il n'a de sens que pour le registre d'état flottant, n'a pas d'équivalent dans le registre d'état entier, et il faut des instructions de test dédiées pour le tester. ==Les méta-instructions REPEAT et EXECUTE== Les '''méta-instructions''' 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 de calcul d'adresse== Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons. ===Les tableaux : des données consécutives en RAM=== Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux. Les '''tableaux''' regroupent plusieurs données de même type, qui 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''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé. Toute donnée dans un 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. Dans ce qui suit, nous supposons que cet indice est dans un registre. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base 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> La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée. ====Le calcul base + indice décalé==== Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent. En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets. Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type : : <math>\text{Adresse de base} + \text{Indice} << n</math> Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler). Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés. Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé. ====Le calcul base + indice décalé + décalage==== Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme : : <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math> L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça. Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons). Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante. ===L'adressage relatif et le code indépendant de la position=== [[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]] Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions : * une qui mémorise le '''code du programme''', à savoir ses instructions ; * une qui mémorise des '''données statiques''', qui sont toujours au même endroit ; * et une autre pour des '''données variables'''. Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution. A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel. Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ? Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ? Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''. Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première. La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM. L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres. Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus. Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate. ==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> s4balwnsyc0ub285z3tsm0r4xe3qazy Fonctionnement d'un ordinateur/Les composants d'un processeur 0 65784 775127 772676 2026-10-13T02:23:38Z Mewtow 31375 /* L'évolution des processeurs dans le temps */ 775127 wikitext text/x-wiki Dans le chapitre sur le langage machine, on a vu le processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres. Mais on n'a pas encore vu ce qu'il y a dans la boite noire. Pour cela, nous allons attaquer la '''micro-architecture''' du processeur, ce qu'il y a à l'intérieur et comment il fait pour exécuter une instruction. [[File:Von Neumann Cyclus.png|vignette|Les trois cycles d'une instruction.]] Le but d'un processeur, c'est d’exécuter des instructions. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Exécuter une instruction est un processeur en trois étapes : * Le processeur charge l'instruction depuis la mémoire : c'est l'étape de '''chargement''' (''Fetch'') ; * Ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à exécuter : c'est l'étape de '''décodage''' (''Decode'') ; * Enfin, le processeur exécute l'instruction : c'est l'étape d’'''exécution''' (''Execute'').* Une quatrième '''étape d'interruption''' s'occupe des interruptions, précisément des interruptions matérielles et des exceptions. Elle est optionnelle, car de rares processeurs ne supportent pas les interruptions. Nous ne parlerons pas de l'étape d'interruption ici et allons nous concentrer sur les trois premières étapes. Il se trouve qu'elles sont réalisées chacune par un circuit séparé des autres. ==La micro-architecture d'un processeur== À l'intérieur du processeur, il y a un circuit dédié pour le chargement de l'instruction, un circuit dédié au décodage des instruction, et un autre pour leur exécution. Ils portent respectivement les noms d'unité de chargement, unité de décodage d'instruction, et de chemin de données. * Le '''chemin de données''' contient de quoi exécuter les instructions. Il regroupe des circuits de calcul, les registres, un circuit de communication avec la mémoire, et des interconnexions entre les circuits précédents. * L’'''unité de contrôle''' regroupe l'unité de chargement et le décodeur d'instruction. L'unité de chargement charge l'instruction depuis la mémoire, le décodeur/séquenceur commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux pour gérer les branchements, pour charger les instructions au bon moment, etc. [[File:Microarchitecture d'un processeur.png|centre|vignette|upright=2|Microarchitecture d'un processeur]] ===Le chemin de données=== Pour effectuer des calculs, le processeur contient un circuit spécialisé : l''''unité de calcul'''. De plus, le processeur contient des '''registres''', ainsi qu'un circuit d''''interface mémoire'''. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble d'interconnexions, afin de pouvoir échanger des informations. Ces interconnexions forment ce qu'on appelle le '''bus interne du processeur'''. L'ensemble formé par ces composants s’appelle le '''chemin de données'''. Son nom vient du fait que les données circulent dans le chemin de données, dans le bus interne, l'unité de calcul, les registres et l'unité mémoire. Pour le dire autrement, il regroupe tous les circuits dans lesquels sont traités des données. [[File:Chemin de données.png|centre|vignette|upright=1.5|Chemin de données]] L'unité de calcul, les registres et l'unité mémoire sont configurables. Par exemple, l'unité de calcul peut faire plusieurs opérations : addition, soustraction, opérations logiques, et bien d'autres. Mais il faut préciser quelle opération effectuer. Pour cela, l'ALU a une entrée de commande sur laquelle on précise l'opération à effectuer. L'opération est encodée en binaire, chaque opération a son propre numéro. Il s'agit d'un '''signal de commande''', qui indique comment configurer l'unité de calcul pour faire ce qui est demandé. Les registres et l'unité mémoire ont aussi leurs propres signaux de commande dédiés. Par exemple, les noms/numéros de registres sont des signaux de commande qui permettent de sélectionner les registres utilisés comme opérande ou pour enregistrer le résultat. De même, l'unité mémoire ne fonctionne que si on lui précise quelle est l'adresse à lire/écrire, et s'il faut faire une lecture ou une écriture : les deux sont des signaux de commande. [[File:Signaux de commandes CPU.png|centre|vignette|upright=2|Signaux de commandes CPU]] ===Le séquenceur : signaux de commande et micro-opérations=== Le chemin de données est en charge de l'exécution des instructions, au minimum (on verra qu'il peut être impliqué dans l'étape de chargement, dans quelques paragraphes). Intuitivement, exécuter une instruction revient à configurer le chemin de données de manière adéquate. Si on veut faire une addition entre un registre et une adresse mémoire, on configure l'unité de calcul pour faire une addition, on configure les registres de manière à sélectionner les registres opérande/destination, et on envoie l'adresse à l'unité mémoire. Le '''décodeur d'instruction''' détermine quels sont les signaux de commande adéquats, à partir de l'instruction machine. Les signaux de commande des registres sont déterminés à partir des noms/numéros de registre encodés dans l'instruction, les signaux pour l'ALU sont déterminés à partir de l'opcode, etc. Le terme décodeur trahit le fait qu'il traduit une instruction machine en signaux de commandes séparés, qui se déduisent de l'instruction elle-même, de son encodage. [[File:Intérieur d'un processeur.png|centre|vignette|upright=2|Intérieur d'un processeur]] Mais cette explication ne marche que pour des instructions simples, qui, s'effectuent en une seule étape. Or, l'exécution d'une instruction complexe se fait en plusieurs étapes distinctes. Suivant l'instruction machine, le nombre d'étapes n'est pas le même. Voyons quelles instructions tendent à se faire une une ou plusieurs étapes, avec quelques exemples. Commençons par l'exemple d'une instruction de lecture en mode d'adressage absolu. L'adresse à lire est envoyée à l'unité mémoire directement, le nom de registre est envoyé aux registres et basta. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Maintenant, voyons la même instruction, mais avec un mode d'adressage base + indice. Avec ce mode d'adressage, l'adresse doit être calculée en additionnant une adresse de base et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire la donnée, l'instruction va devoir calculer l'adresse, dans l'unité de calcul. L'étape d’exécution s'effectue dorénavant en deux étapes : une étape de calcul d'adresse, suivie de la lecture en mémoire. Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une '''micro-opération''', ou encore une micro-instruction. Les micro-opérations se résument à des opérations basiques : échange de données entre registres, calcul sur l'ALU, accès mémoire. Chaque micro-opération encode les signaux de commande à destination du chemin de données, pour le configurer et le commander. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Signaux de commande autres |} Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. C'est le décodeur d'instruction qui transforme une instruction machine en une série de micro-opérations. Pour cela, le décodeur devient un circuit séquentiel. Il est parfois appelé le '''séquenceur''', terme qui trahit bien le fait qu'il traduit une instruction machine en une séquence de micro-opérations. [[File:Micro-operations.svg|centre|vignette|upright=2|Micro-operations]] Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles. Typiquement, les instructions des processeurs RISC se font en une seule micro-opération, alors que les instructions complexes des processeurs CISC demandent plusieurs micro-opérations, particulièrement celles qui demandent de faire des accès mémoire ou qui utilisent des modes d'adressage complexes. Ce n'est pas une règle absolue, quelques instructions RISC se font en plusieurs micro-opérations, de même que les processeurs CISC ont des instructions simples qui se font en une seule micro-opération. Mais la corrélation est assez bonne. En conséquence, les processeurs RISC ont des décodeurs d'instruction très simples. ===L'unité de chargement=== [[File:Microarchitecture d'un processeur basique.png|vignette|Processeur basique, avec une unité de chargement.]] L''''unité de chargement''' est placée avant le décodeur/séquenceur. Son rôle est de charger les instructions les unes après les autres, dans le bon ordre. Pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé '''''program counter'''''. L'adresse de la prochaine instruction ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au ''program counter''. Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle. [[File:Encodage d'une instruction sur un processeur sans Program Counter.png|centre|vignette|upright=2|Encodage d'une instruction sur un processeur sans Program Counter.]] L'unité de chargement s'occupe de tout ce qui a trait au ''program counter'', mais aussi de l'accès mémoire pour charger l'instruction. Il faut noter que sur certains processeurs, le chargement d'une instruction machine est une opération réalisée par le chemin de données. C'est le cas sur les architectures Von Neumann, où il n'y a qu'un seul bus mémoire, qui sert à la fois pour lire/écrire des données et pour charger des instructions. Dans ce cas, l'unité de communication avec la mémoire s'occupe aussi de charger les instructions. Le chargement d'une instruction est alors réalisée par une micro-opération d'accès mémoire, qui copie l'instruction chargée dans un registre dédié, appelé le registre d'instruction, lui-même relié au séquenceur. ===Les autres circuits=== Un processeur contient au minimum une unité de chargement, le chemin de données et le décodeur d'instruction. Mis c'est là le minimum, un processeur peut parfaitement contenir d'autres circuits en plus. Par exemple, il peut intégrer des circuits pour gérer les interruptions matérielles, des circuits ''timer'' pour compter des durées, des circuits dit DMA pour communiquer avec les périphériques, etc. L'exemple du 80186 d'Intel est illustré ci-dessous. Il est composé de deux circuits principaux : une ''Execution Unit'' qui regroupe l'unité de calcul et les registres, une ''Bus Interface Unit'' qui regroupe unité mémoire, unité de chargement et séquenceur. Mais en plus de ces deux circuits principaux, le processeur incorpore des circuits ''timers'', de quoi gérer les interruption, un circuit DMA, et bien d'autres encore. [[File:Intel 80186 80188 arch.svg|centre|vignette|upright=2.5|Intel 80186 80188 arch]] ==Des processeurs vendus en kit aux premiers microprocesseurs== Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique. ===Avant l'invention du microprocesseur=== Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900. Les ALUs en pièces détachées de l'époque étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du '''''bit slicing''''' que nous avions abordée dans le chapitre sur les unités de calcul. ===L'intel 4004 : le premier microprocesseur=== Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les '''microprocesseurs''', à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'''Air data computer''. Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. Il comprenait environ 2300 transistors, avait une fréquence de 740 MHz, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il pouvait faire 46 opérations différentes. C'était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc. Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits. ===L'évolution des processeurs dans le temps=== La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors. Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce. De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur. Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres. Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs. Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Un exemple de jeu d'instruction : l'extension x87 | prevText=Un exemple de jeu d'instruction : l'extension x87 | next=Le chemin de données | nextText=Le chemin de données }} </noinclude> 9n59mai4ix4rsupv4qxxs2mmkc6wqdn Fonctionnement d'un ordinateur/Le préchargement 0 65804 775106 772484 2026-10-12T23:26:23Z Mewtow 31375 /* La pollution du cache */ 775106 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> jk9nirp3ddi2hhobh4593yuckkwdqfj 775107 775106 2026-10-12T23:30:39Z Mewtow 31375 /* Quand précharger ? */ 775107 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetcher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le préchargement par prédiction marche nettement mieux pour précharger des données dans le cache L1, mais moins pour les niveaux de cache inférieurs. Il y a plusieurs raisons à cela. La première est que pour le cache L1, les accès aux données et chargement d'instructions sont séparés. Il est possible d'utiliser des ''prefetchers'' séparés pour les données et les instructions, chacun se basant sur des historiques séparés. Pour les niveaux en-dessous, on n'a pas le choix : on doit utiliser un seul ''prefetcher'', qui ne fera pas la différence entre données et instructions. Une autre raison est que le cache L1 filtre les accès mémoire. Les succès dans le cache L1 sont invisibles pour le ''prefetcher'' associé au cache L2. Il ne voit pas ces accès mémoire, seulement les accès qui font un défaut dans le cache L1. Et cela fait des informations en moins, qu'il aurait pu utiliser pour améliorer ses prédictions. Le ''prefetcher'' des caches L1 voit tous les accès mémoire, sans exception, il; est donc plus efficace. Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> iu08jq9bc28us20h9opmvm2vk8hx6s8 775108 775107 2026-10-12T23:31:19Z Mewtow 31375 /* Quand précharger ? */ 775108 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetcher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le préchargement par prédiction marche nettement mieux pour précharger des données dans le cache L1, mais moins pour les niveaux de cache inférieurs. Il y a plusieurs raisons à cela. La première est que pour le cache L1, les accès aux données et chargement d'instructions sont séparés. Il est possible d'utiliser des ''prefetchers'' séparés pour les données et les instructions, chacun se basant sur des historiques séparés. Pour les niveaux en-dessous, on n'a pas le choix : on doit utiliser un seul ''prefetcher'', qui ne fera pas la différence entre données et instructions. Une autre raison est que le cache L1 filtre les accès mémoire. Les succès dans le cache L1 sont invisibles pour le ''prefetcher'' associé au cache L2. Il ne voit pas ces accès mémoire, seulement les accès qui font un défaut dans le cache L1. Et cela fait des informations en moins, qu'il aurait pu utiliser pour améliorer ses prédictions. Le ''prefetcher'' des caches L1 voit tous les accès mémoire, sans exception, il; est donc plus efficace. Le '''préchargement sur événement''' précharge des données lors d'un défaut de cache, ou plus généralement quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> gxjzgmtyv3o9ip2qxpnnj8wd1xqd41h 775109 775108 2026-10-12T23:38:08Z Mewtow 31375 /* Quand précharger ? */ 775109 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetcher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le '''préchargement sur événement''' précharge des données lors d'un défaut de cache, ou plus généralement quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement. ===Le préchargement par prédiction=== Le préchargement par prédiction marche nettement mieux pour précharger des données dans le cache L1, mais moins pour les niveaux de cache inférieurs. Il y a plusieurs raisons à cela. La première est que pour le cache L1, les accès aux données et chargement d'instructions sont séparés. Il est possible d'utiliser des ''prefetchers'' séparés pour les données et les instructions, chacun se basant sur des historiques séparés. Pour les niveaux en-dessous, on n'a pas le choix : on doit utiliser un seul ''prefetcher'', qui ne fera pas la différence entre données et instructions. Une autre raison est que le cache L1 filtre les accès mémoire. Les succès dans le cache L1 sont invisibles pour le ''prefetcher'' associé au cache L2. Il ne voit pas ces accès mémoire, seulement les accès qui font un défaut dans le cache L1. Et cela fait des informations en moins, qu'il aurait pu utiliser pour améliorer ses prédictions. Le ''prefetcher'' des caches L1 voit tous les accès mémoire, sans exception, il; est donc plus efficace. La prédiction ne fait pas que déterminer quoi précharger, mais aussi s'il faut le faire. Il existe des techniques de préchargement dynamiques qui s'activent seulement quand elles sont assez efficaces, mais se mettent en pause si elles deviennent contre-productives. Une implémentation est celle des CPU utilisés dans les ''System on Chip'' Exynos. Le ''prefetcher'' est en réalité composé de deux ''prefetcher'', dont un seul a le pouvoir de précharger des données. Le premier ''prefetcher'' marche tout le temps, mais n'a pas accès à la mémoire. Il fait des prédictions, que le second ''prefetch'' vérifie, qu'il compare avec les accès mémoire réels. Si le premier ''prefetch'' a souvent raison, le second accède à la mémoire en utilisant ses prédictions. Mais si les prédictions et la réalité commencent à diverger, e second ''prefetcher'' filtre les prédictions, voire cesse de les utiliser. Le préchargement cesse si les prédictions divergent trop de la réalité. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> hesbqublxmoolo0w6amt8rb76dpwnfu 775110 775109 2026-10-12T23:41:40Z Mewtow 31375 /* Le préchargement par prédiction */ 775110 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetcher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le '''préchargement sur événement''' précharge des données lors d'un défaut de cache, ou plus généralement quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement. ===Le préchargement par prédiction=== Le préchargement par prédiction marche nettement mieux pour précharger des données dans le cache L1, mais moins pour les niveaux de cache inférieurs. Il y a plusieurs raisons à cela. La première est que pour le cache L1, les accès aux données et chargement d'instructions sont séparés. Il est possible d'utiliser des ''prefetchers'' séparés pour les données et les instructions, chacun se basant sur des historiques séparés. Pour les niveaux en-dessous, on n'a pas le choix : on doit utiliser un seul ''prefetcher'', qui ne fera pas la différence entre données et instructions. Une autre raison est que le cache L1 filtre les accès mémoire. Les succès dans le cache L1 sont invisibles pour le ''prefetcher'' associé au cache L2. Il ne voit pas ces accès mémoire, seulement les accès qui font un défaut dans le cache L1. Et cela fait des informations en moins, qu'il aurait pu utiliser pour améliorer ses prédictions. Le ''prefetcher'' des caches L1 voit tous les accès mémoire, sans exception, il; est donc plus efficace. La prédiction ne fait pas que déterminer quoi précharger, mais aussi s'il faut le faire. Il existe des techniques de préchargement dynamiques qui s'activent seulement quand elles sont assez efficaces, mais se mettent en pause si elles deviennent contre-productives. Une implémentation est celle des CPU utilisés dans les ''System on Chip'' Exynos. Le ''prefetcher'' est composé du ''prefetcher'' proprement dit et d'une unité de vérification. Le ''prefetcher'' marche tout le temps, son accès à la mémoire lui est accordé par l'unité de vérification. L'unité de vérification regarde si les prédictions du ''prefetcher'' se réalisent. Si le ''prefetch'' a souvent raison, l'accès à la mémoire lui est accordé et le préchargement a lieu. Mais si les prédictions et la réalité commencent à diverger, l'unité de vérification peut lui couper l'accès à la mémoire, le préchargement cesse si les prédictions divergent trop de la réalité. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> oapna12vrgx23ox60cymk9dmls5f6kr 775111 775110 2026-10-12T23:43:30Z Mewtow 31375 /* Quand précharger ? */ 775111 wikitext text/x-wiki En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles. Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ? Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données. Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre. Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante. ==Les ''Prefetchers'' séquentiels== Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe. [[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]] ===Le préchargement séquentiel pour les instructions=== Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions. Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] ===L'interaction avec la mémoire virtuelle=== Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ? La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page. Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine. ===Les ''stream buffers''=== L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés. La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement. [[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]] ===Le préchargement adaptatif=== Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. * Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. * Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro. ==Les ''prefetchers'' spécialisés pour les données== Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section. ===Les accès par enjambées=== Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup. Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache. Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement. L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction. [[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]] Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''. [[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]] On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée. [[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]] ===Le préchargement selon les dépendances=== Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données. La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée). {| |[[File:Instruction productrice.jpg|vignette|Instruction productrice.]] |[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]] |} Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire. ===Le préchargement de Markov=== Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée. Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables. ===Le préchargement par distance=== Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses. [[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]] Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes. ===Le Tampon d’historique global=== Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe. Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus. Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe. [[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]] Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable. [[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]] La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare. Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles. ===Les variantes du tampon d’historique global=== Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index : * la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ; * la seconde prend en entrée l'adresse à lire ou écrire. [[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]] ==La pollution du cache== Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent. Une première solution élimine les données préchargées du cache en priorité. Les données préchargées sont sélectionnées pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ». Il est aussi possible de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache. Le '''filtrage de cache''' détecte les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion. ==Quand précharger ?== Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction. Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetcher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués. Le '''préchargement sur événement''' précharge des données lors d'un défaut de cache, ou plus généralement quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. ===Le préchargement par prédiction=== Le préchargement par prédiction marche nettement mieux pour précharger des données dans le cache L1, mais moins pour les niveaux de cache inférieurs. Il y a plusieurs raisons à cela. La première est que pour le cache L1, les accès aux données et chargement d'instructions sont séparés. Il est possible d'utiliser des ''prefetchers'' séparés pour les données et les instructions, chacun se basant sur des historiques séparés. Pour les niveaux en-dessous, on n'a pas le choix : on doit utiliser un seul ''prefetcher'', qui ne fera pas la différence entre données et instructions. Une autre raison est que le cache L1 filtre les accès mémoire. Les succès dans le cache L1 sont invisibles pour le ''prefetcher'' associé au cache L2. Il ne voit pas ces accès mémoire, seulement les accès qui font un défaut dans le cache L1. Et cela fait des informations en moins, qu'il aurait pu utiliser pour améliorer ses prédictions. Le ''prefetcher'' des caches L1 voit tous les accès mémoire, sans exception, il; est donc plus efficace. La prédiction ne fait pas que déterminer quoi précharger, mais aussi s'il faut le faire. Il existe des techniques de préchargement dynamiques qui s'activent seulement quand elles sont assez efficaces, mais se mettent en pause si elles deviennent contre-productives. Une implémentation est celle des CPU utilisés dans les ''System on Chip'' Exynos. Le ''prefetcher'' est composé du ''prefetcher'' proprement dit et d'une unité de vérification. Le ''prefetcher'' marche tout le temps, son accès à la mémoire lui est accordé par l'unité de vérification. L'unité de vérification regarde si les prédictions du ''prefetcher'' se réalisent. Si le ''prefetch'' a souvent raison, l'accès à la mémoire lui est accordé et le préchargement a lieu. Mais si les prédictions et la réalité commencent à diverger, l'unité de vérification peut lui couper l'accès à la mémoire, le préchargement cesse si les prédictions divergent trop de la réalité. ===Le préchargement associé aux accès mémoire=== Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles. Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace. Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1. [[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]] ===Le préchargement anticipé (''runahead prefetching'')=== La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance. Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées. Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM. : Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares. Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''. Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache. Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles. Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides. Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas. Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent. : Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires cache | prevText=Les mémoires cache | next=Le Translation Lookaside Buffer | nextText=Le Translation Lookaside Buffer }} </noinclude> 4td5jbvxm00sl44ygcs3xlvk57orqfo Fonctionnement d'un ordinateur/L'émission des instructions 0 65843 775058 775008 2026-10-12T16:51:19Z Mewtow 31375 /* L'exécution parallèle avec des instructions multi-cycles */ 775058 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair à la fin du chapitre. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ===Le blocage du pipeline=== Une contrainte est que pour émettre une micro-opération dans une voie, celle-ci doit être inoccupée. Or, si la voie est en train d’exécuter une opération, elle ne peut pas en accepter de nouvelles. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. Si une micro-opération veut utiliser une unité occupée, il y a une dépendance structurelle. La micro-opération fautive est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Une micro-opération est émise dans la voie adaptée, si cette voie est libre. Mais l'usage d'instructions multi-cycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> r6iswposm7r7m9v3f1vp0pe8vleu8yp 775060 775058 2026-10-12T16:52:31Z Mewtow 31375 /* Le blocage du pipeline */ 775060 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair à la fin du chapitre. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ===Le blocage du pipeline=== Une contrainte est que pour émettre une micro-opération dans une voie, celle-ci doit être inoccupée. Or, si la voie est en train d’exécuter une opération, elle ne peut pas en accepter de nouvelles. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. Si une micro-opération veut utiliser une unité occupée, il y a une dépendance structurelle. La micro-opération fautive est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Une micro-opération est émise dans la voie adaptée, si cette voie est libre. Mais l'usage d'instructions multi-cycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 5734os2a04wntsewdnz93xefx9bhvbl 775061 775060 2026-10-12T16:58:58Z Mewtow 31375 775061 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair à la fin du chapitre. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 3uodd6ipn2ier64z6pw411u04oko2do 775062 775061 2026-10-12T17:07:22Z Mewtow 31375 /* L'émission des instructions et l'unité d'émission */ 775062 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> eqiookqzuzj4najx37ep2opzqv0tvht 775065 775062 2026-10-12T18:02:54Z Mewtow 31375 /* L'exécution parallèle avec des instructions multi-cycles */ 775065 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. {| |[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] |} Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 3 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="3" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="3" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec |} ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> to7zwjuln9egmbnu6dt7btxcdjct0qr 775066 775065 2026-10-12T18:10:34Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775066 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. {| |[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] |} Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Les deux partageaient l'unité de chargement et l'étage de décodage, mais tout ce qui se passe après est totalement séparé. Les registres sont différents, les unités de calcul aussi, il est même possible d'utiliser une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. Avec un co-processeur intégré, l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 2vf28lova4oeqmwjyujj3tvf1zen4we 775067 775066 2026-10-12T18:13:39Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775067 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. {| |[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] |} Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> k8ebf0ge9bbdriqu65m05om7tk9p38v 775068 775067 2026-10-12T18:18:10Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775068 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. {| |[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] |} Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> mqcll8mo7moih2nrb7lg629q1nqq322 775069 775068 2026-10-12T18:18:22Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775069 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> tvbq0nyakfc2wgd2lzjhu2ezzrj1pf7 775070 775069 2026-10-12T18:28:30Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775070 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. les écritures dans les registres entiers et flottantes sont découplées par nature, donc découpler les pipelines passe. ===L'exécution parallèle avec des instructions multi-cycles=== Les processeurs gèrent presque tous des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 4pg40olaolrs9iidlmrzpja9g258ghd 775071 775070 2026-10-12T18:28:59Z Mewtow 31375 /* L'exécution parallèle avec des instructions multi-cycles */ 775071 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. les écritures dans les registres entiers et flottantes sont découplées par nature, donc découpler les pipelines passe. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles simplement. Il suffit concrètement d'ajouter un multiplieur entier, une FPU, éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite, ou du ''barrel shifter''. L’avantage est que le processeur ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> gwtreyi24gbe7ch654h69m0qqd7bnjz 775072 775071 2026-10-12T18:30:43Z Mewtow 31375 /* L'exécution parallèle avec des instructions multi-cycles */ 775072 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. les écritures dans les registres entiers et flottantes sont découplées par nature, donc découpler les pipelines passe. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 74ol2oi4d7wenpcz9lylz4hkx3jjtp0 775074 775072 2026-10-12T18:46:53Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775074 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> i12e8z4a62xp3xxks852ahiks9x27r1 775077 775074 2026-10-12T19:10:15Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775077 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le décodeur flottant. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une unité d'émission flottante. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. La technique était utilisée sur les vieux processeurs ARM et leurs coprocesseurs VFP et NEON ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> park64xjzve7vcb0wk8r84x16bonxk7 775078 775077 2026-10-12T19:18:29Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775078 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le ''décodeur flottant''. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une ''unité d'émission flottante''. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Quelques processeurs poussent le découplage au point d'ajouter une file d'instruction en aval du décodeur flottant. Les instructions provenant du décodeur principal sont accumulées dans cette file d'instruction, ce qui fait que le pipeline flottant a quelques cycles de retard sur le pipeline entier, retarde qui est de plus variable. MAis un tel retard ne pose pas de problèmes en général, car les registres entiers et flottants sont séparés. Il n'y a synchronisation que quand les deux pipelines doivent échanger des données. Les échanges de données entre les deux pipelines se font avec copies entre registres spécialisées, qui copient un registre entier vers un registre flottant, et inversement. En clair, des instructions spécialisées qui gèrent la situation au mieux. Elles sont exécutées dans le pipeline entier. Lorsque le processeur décode une de ces instructions, il attend que le pipeline flottant ait terminé son travail, avant de poursuivre l'exécution. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 525dmdtodbqlx4gtxutz3cne8j2v4m3 775083 775078 2026-10-12T19:52:39Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775083 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Le MIPS R4000 utilisait cette méthode pour son coprocesseur flottant intégré. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le ''décodeur flottant''. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une ''unité d'émission flottante''. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Quelques processeurs poussent le découplage au point d'ajouter une file d'instruction en aval du décodeur flottant. Les instructions provenant du décodeur principal sont accumulées dans cette file d'instruction, ce qui fait que le pipeline flottant a quelques cycles de retard sur le pipeline entier, retarde qui est de plus variable. MAis un tel retard ne pose pas de problèmes en général, car les registres entiers et flottants sont séparés. Il n'y a synchronisation que quand les deux pipelines doivent échanger des données. Les échanges de données entre les deux pipelines se font avec copies entre registres spécialisées, qui copient un registre entier vers un registre flottant, et inversement. En clair, des instructions spécialisées qui gèrent la situation au mieux. Elles sont exécutées dans le pipeline entier. Lorsque le processeur décode une de ces instructions, il attend que le pipeline flottant ait terminé son travail, avant de poursuivre l'exécution. Il est possible que le pipeline entier ait des exceptions précises, alors que le pipeline flottant n'en ait pas. Les exceptions flottantes sont en effet bien séparées des exceptions entières. Et les deux peuvent être traitées séparément. D'anciens processeurs faisaient comme cela, comme le SuperSPARC de Texas Instrument. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> jl7o9skgzcr7vc6hl7lmtnu9orhxww7 775096 775083 2026-10-12T22:04:08Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775096 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le ''décodeur flottant''. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une ''unité d'émission flottante''. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Quelques processeurs poussent le découplage au point d'ajouter une file d'instruction en aval du décodeur flottant. Les instructions provenant du décodeur principal sont accumulées dans cette file d'instruction, ce qui fait que le pipeline flottant a quelques cycles de retard sur le pipeline entier, retarde qui est de plus variable. Mais un tel retard ne pose pas de problèmes en général, car les registres entiers et flottants sont séparés. Il n'y a synchronisation que quand les deux pipelines doivent échanger des données. Les échanges de données entre les deux pipelines se font avec copies entre registres spécialisées, qui copient un registre entier vers un registre flottant, et inversement. En clair, des instructions spécialisées qui gèrent la situation au mieux. Elles sont exécutées dans le pipeline entier. Lorsque le processeur décode une de ces instructions, il attend que le pipeline flottant ait terminé son travail, avant de poursuivre l'exécution. : Sur le processeur alpha 61064, il n'y avait même pas de système de copie entre registres entier et flottants. Les échanges en question devaient passer par la mémoire RAM. Il est possible que le pipeline entier ait des exceptions précises, alors que le pipeline flottant n'en ait pas. Les exceptions flottantes sont en effet bien séparées des exceptions entières. Et les deux peuvent être traitées séparément. D'anciens processeurs faisaient comme cela, comme le SuperSPARC de Texas Instrument. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 1mo1p7dma747rwsg3fcnwpw9ayiwmyh 775104 775096 2026-10-12T22:54:52Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775104 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Les échanges de données entre les deux pipelines se font avec copies entre registres spécialisées, qui copient un registre entier vers un registre flottant, et inversement. En clair, des instructions spécialisées qui gèrent la situation au mieux. Elles sont exécutées dans le pipeline entier. Lorsque le processeur décode une de ces instructions, il attend que le pipeline flottant ait terminé son travail, avant de poursuivre l'exécution. : Sur le processeur alpha 61064, il n'y avait même pas de système de copie entre registres entier et flottants. Les échanges en question devaient passer par la mémoire RAM. Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. Il est possible que le pipeline entier ait des exceptions précises, alors que le pipeline flottant n'en ait pas. Les exceptions flottantes sont en effet bien séparées des exceptions entières. Et les deux peuvent être traitées séparément. D'anciens processeurs faisaient comme cela, comme le SuperSPARC de Texas Instrument. Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le ''décodeur flottant''. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une ''unité d'émission flottante''. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Quelques processeurs poussent le découplage au point d'ajouter une file d'instruction en aval du décodeur flottant. Les instructions provenant du décodeur principal sont accumulées dans cette file d'instruction, ce qui fait que le pipeline flottant a quelques cycles de retard sur le pipeline entier, retarde qui est de plus variable. Mais un tel retard ne pose pas de problèmes en général, car les registres entiers et flottants sont séparés. Il n'y a synchronisation que quand les deux pipelines doivent échanger des données. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> 5oxa24k487jpykrn68p1hnlepijlwcp 775105 775104 2026-10-12T23:17:38Z Mewtow 31375 /* Les pipelines entiers et flottants */ 775105 wikitext text/x-wiki Un processeur avec un pipeline est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. [[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec plusieurs avals et un amont (back-end et front-end).]] Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat. {| |[[File:Pipeline à deux aval de type load-store.png|vignette|upright=1.5|Pipeline à trois avals.]] |[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|vignette|upright=1.5|Pipeline avec trois avals, bis.]] |} Et cette structuration en amont et avals est à l'origine de nombreuses optimisations. Nous verrons dans quelques chapitres que l'on peut découpler les deux, avec une file de µops. Mais n'allons pas trop vite en besogne. Pour le moment, nous allons juste parler de la manière dont sont réparties les instructions sur plusieurs avals. ==L'émission des instructions et l'unité d'émission== Le processeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie. Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données. Le processeur contient une '''unité d'émission''', en sortie du décodeur. Elle décide dans quelle voie envoyer telle ou telle micro-opération, mais pas seulement. Son rôle deviendra nettement plus clair dans quelques chapitres. Je l'introduis précocement histoire de simplifier les explications qui suivent. Pour le moment, il est juste évident qu'elle sert de ''répartiteur'', pour envoyer la micro-opération dans la bonne unité de calcul, le bon aval, la bonne voie. Mais nous verrons que c'est cette unité qui garantit que les instructions se terminent dans l'ordre. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] ===Les pipelines entiers et flottants=== Un processeur a souvent deux avals séparés : un pour les opérations flottantes, et un autre pour le reste. Le reste en question regroupe à la fois les calculs entiers, les tests/branchements, les accès mémoire, et éventuellement d'autres instructions. Les deux avals sont appelés, par abus de langage, le '''pipeline flottant''' et le '''pipeline entier'''. Le pipeline flottant exécute les instructions flottantes, à l'exception des lectures/écritures flottantes, qui sont prises en charge par le pipeline entier. Il peut aussi exécuter les multiplications entières si elles sont prises en charge par le multiplieur flottant. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Un détail important est que les deux avals n'ont pas forcément le même nombre d'étages. Le pipeline flottant a souvent des étages en plus,car la FPU met plusieurs cycles pour faire son travail. A l'opposé, les instructions entières se font plus rapidement, souvent en un cycle pour les opérations simples. Un exemple est le pipeline du MIPS R4000. Son pipeline entier faisait 8 étages, le pipeline flottant avait 2 à 6 cycles en plus. Les étages de chargement et décodage étaient partagé, mais les deux avals avaient des nombres d'étages différents. {|class="wikitable" |- ! !! colspan="2" | Instruction fetch !! Decocage !! colspan="6" | Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- ! Pipeline entier | rowspan="2" | ''Fetch'' 1 || rowspan="2" | ''Fetch'' 2 || rowspan="2" | ''Decode'' + ''Read Operands'' || colspan="6" | ''Exec'' || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Data Fetch'' 1 || rowspan="2" | ''Tag Check'' || rowspan="2" | ''Writeback'' |- ! Pipeline flottant | Exec || Exec || Exec || Exec || Exec || Exec |} Les échanges de données entre les deux pipelines se font avec copies entre registres spécialisées, qui copient un registre entier vers un registre flottant, et inversement. En clair, des instructions spécialisées qui gèrent la situation au mieux. Elles sont exécutées dans le pipeline entier. Lorsque le processeur décode une de ces instructions, il attend que le pipeline flottant ait terminé son travail, avant de poursuivre l'exécution. : Sur le processeur alpha 61064, il n'y avait même pas de système de copie entre registres entier et flottants. Les échanges en question devaient passer par la mémoire RAM. Les deux pipelines peuvent être totalement indépendants, ou au contraire fortement couplés. Il arrive que les deux pipelines ne peuvent pas fonctionner en parallèle. Quand l'un exécute des instructions, l'autre ne fait rien. Si une instruction est en cours de traitement dans la FPU, le pipeline entier cesse toute activité, il n’exécute pas d'instructions. Et inversement, quand le pipeline entier travaille, la FPU est en stand-by, elle n’exécute pas d'instructions flottantes. Une autre méthode autorise le pipeline flottant et le pipeline entier à travailler en parallèle. Pendant que la FPU exécute des opérations flottantes, le pipeline entier peut exécuter des instructions. Cependant, faites attention : les deux pipelines démarrent une instruction par cycle. Le truc est que les instructions flottantes sont des instructions multi-cycles. Pendant qu'une instruction flottante s'exécute dans la FPU, le processeur peut démarrer une nouvelle instruction entière dans le pipeline flottant à chaque cycle. Les deux pipelines agissant sur des registres différents, cela ne pose pas de problème. Il est possible que le pipeline entier ait des exceptions précises, alors que le pipeline flottant n'en ait pas. Les exceptions flottantes sont en effet bien séparées des exceptions entières. Et les deux peuvent être traitées séparément. D'anciens processeurs faisaient comme cela, comme le SuperSPARC de Texas Instrument. Il est arrivé que le pipeline flottant soit traité comme un '''co-processeur intégré''' au processeur principal. Il y a deux grosses différences avec un aval flottant séparé. La première est qu'il y a une unité mémoire séparée dans le pipeline flottant. Le processeur peut alors faire lectures et écritures flottantes dans le pipeline flottant. La seconde est que l'unité flottante a une unité de contrôle dédiée, avec son propre décodeur et sa propre unité d'émission. Il y a donc deux décodeurs : un décodeur principal, et le ''décodeur flottant''. Le décodeur principal décode toutes les instructions, sauf les instructions flottantes. Quand il détecte une instruction flottante, il l'envoie à l'unité de contrôle flottante, et c'est elle qui se charge du décodage. Et après ce décodeur flottant, il y a une ''unité d'émission flottante''. Elle s'occupe de répartir les instructions dans la bonne unité flottante, elle vérifie les dépendances structurelles, bref : c'est une unité d'émission classique, qui ne s'occupe que des FPUs. [[File:Pipeline flottant fortement découplé.png|centre|vignette|upright=1.5|Pipeline flottant fortement découplé.]] Quelques processeurs poussent le découplage au point d'ajouter une file d'instruction en aval du décodeur flottant. Le MIPS R8000 était de ceux là. Les instructions provenant du décodeur principal sont accumulées dans cette file d'instruction, ce qui fait que le pipeline flottant a quelques cycles de retard sur le pipeline entier, retarde qui est de plus variable. Mais un tel retard ne pose pas de problèmes en général, car les registres entiers et flottants sont séparés. Il n'y a synchronisation que quand les deux pipelines doivent échanger des données. Pour résumer, il y a deux pipelines, entiers et flottants, qui travaillent en parallèle, voire chacun à leur rythme avec des files d'instruction différentes. Il s'agit d'une forme limitée d''''exécution dans le désordre''', cette dernière étant une optimisation qui permet d'exécuter des instructions dans un ordre différent de celui du programme exécuté. Elle permet de gagner en performance, mais causent des problèmes pour les exceptions précises. Par exemple, le MIPS R8000 avait un pipeline flottant totalement désynchronisé avec le pipeline entier, mais il utilisait des exceptions imprécises. Il est aussi possible de charger et exécuter deux instructions en même temps, la première allant dans le pipeline entier, la seconde dans le pipeline flottant. Il s'agit d'une forme limitée de '''superscalarité''', une optimisation dont le principe est de charger, décoder, émettre et exécuter plusieurs instructions en même temps. Ici, une instruction entière et une flottante. Les processeurs dits superscalaires vont cependant plus loin que ça dans le cas général, il ne s'agit ici que d'une forme limitée. ===L'exécution parallèle avec des instructions multi-cycles=== Le pipeline entier peut lui aussi gérer des '''instructions multi-cycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. En pratique, ces instructions sont les multiplications, divisions, et les instructions flottantes. Il arrive aussi que les décalages/rotations prennent deux cycles d'horloge, mais ce n'est pas la norme. Les processeurs sans instructions multi-cycles sont rares. La présence de plusieurs avals permet de gérer les instructions multi-cycles entières simplement. Il suffit d'ajouter un multiplieur entier, un ''barrel shifter'', éventuellement un circuit diviseur. Ils ont tous leur voie réservée, séparée de la voie pour l'ALU entière proprement dite. Et cela permet des optimisations similaires à celle que permettent la séparation du pipeline entier et flottant. L’avantage principal est que le pipeline entier ne se bloque pas pendant qu'on exécute une instruction multi-cycle. Le pipeline impose d'exécuter une nouvelle instruction par cycle, dans une unité de calcul. Si un multiplieur est occupé pendant plusieurs cycles d'horloge, on peut quand même lancer une addition par cycle dans l'ALU entière, ou un décalage dans le ''barrel shifter''. En clair, avec des instructions multi-cycles, le pipeline exploite plusieurs voies en même temps, c'est même la norme. Par exemple, prenons un processeur avec une unité mémoire et une ALU entière, sans même d'ALU pour des opérations multicycle. Il est possible de faire des calculs dans l'ALU pendant que l'unité mémoire fait une lecture/écriture. La lecture est lancée à un cycle, les opérations de l'ALU sont lancées dans les cycles suivants. Une contrainte majeure est que l'addition et la lecture/écriture soient indépendants. Par exemple, l'addition ne doit pas avoir la donnée lue comme opérande. Si c'est le cas, on doit attendre que la donnée soit lue avant de lancer l'addition, sinon cela ne fonctionne pas. Autre exemple, cette fois-ci avec des opérations multi-cycles. Prenons un processeur avec une ALU entière, un multiplieur entier et une unité mémoire. Le processeur peut émettre une micro-opération mémoire lors du cycle 1, puis une multiplication au cycle 2, et une addition au cycle 3. L'accès mémoire va occuper l'unité mémoire pendant plusieurs cycles, idem pour la multiplication. Lors du cycle 3, l'unité mémoire sera encore en train de faire un accès mémoire, le multiplieur sera en pleine multiplication, et l'ALU entière fera une opération entière. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. ==Les dépendances structurelles : l'occupation de chaque voie== Un pipeline peut démarrer une nouvelle instruction par cycle, du moment qu'il a un aval disponible pour. Cependant, il faut tenir compte d'un léger problème : une instructions multi-cycles occupe une voie pendant plusieurs cycles. Et cela nous amène à parler des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Ici, nous allons nous intéresser du cas où une instructions veut utiliser un aval déjà occupé par une instruction multicycle. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. En clair, deux instructions multi-cycles consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs. Sauf à pipeliner l'unité de calcul, mais nous mettons cette option de côté pour le moment. ===Le blocage du pipeline=== Si une micro-opération veut utiliser une unité occupée, il y a une ''dépendance structurelle''. La micro-opération est alors mise en attente, elle attend sagement dans l'unité d'émission que l'unité voulue soit libre. Mais si elle reste dans l'unité d'émission, elle bloque tous les étages précédents, les instructions précédentes ne peuvent pas avancer. Le pipeline est bloqué. Lorsque le pipeline est bloqué, les instructions cessent de progresser d'un étage à l'autre, elles restent dans l'étage courant. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Bloquer le pipeline demande de deux faire plusieurs choses : bloquer la propagation des instructions dans le pipeline, décider quand bloquer le pipeline et pour combien de temps. Bloquer la propagation des instructions consiste à geler les registres de pipeline, ceux entre les étages. Quand ils sont gelés, les registres de pipeline n'acceptent pas ce qui vient de l'étage précédent, ils ne tiennent pas compte de ce qu'il y a sur leur entrée, ils conservent leur valeur pendant toute la durée du blocage. Le processeur génère un signal qui dit aux registres de pipeline dans quel mode fonctionner : soit propager normalement les instructions, soit se geler et rester sur place. Il y a plusieurs méthodes pour geler les registres. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''pipeline stall'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline. Mais laissons cela pour la suite du chapitre. ===La gestion de la disponibilité des ALUs=== L'étage d'émission détecte les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles. Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline. Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''. {|class="wikitable" |+ Vecteur de disponibilité des ALUs |- ! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- | 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs. {|class="wikitable" |+ Masque par instruction, associé au vecteur de disponibilité précédent |- ! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY |- ! ADD | 1 || 1 || 0 || 0 || 0 |- ! MUL | 0 || 0 || 1 || 0 || 0 |- ! COMP | 0 || 0 || 0 || 1 || 0 |- ! LOAD | 0 || 0 || 0 || 0 || 1 |- ! STORE | 0 || 0 || 0 || 0 || 1 |- |} Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0. Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire. ===Pipeliner les unités de calcul=== En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique. La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle. [[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]] Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant. ==La fréquence des avals des pipelines multicycle== L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations. ===Le ''clock gating'' des avals inutilisés=== Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''. Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache. Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge. La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie. ===Les unités de calcul à double fréquence du Pentium 4=== Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe. L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles. La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les optimisations du chargement des instructions | prevText=Les optimisations du chargement des instructions | next=Les pipelines in-order | nextText=Les pipelines in-order }} </noinclude> htzx4ox2qerl0wlpo3i38a5dp20sy7w Fonctionnement d'un ordinateur/L'exécution dans le désordre 0 65869 775076 775011 2026-10-12T19:04:33Z Mewtow 31375 775076 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 : * 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'''. Le terme anglais exact est ''Out Of Order Execution'' et son abréviation OOO. Et nous utiliserons ce terme dans la suite du cours, histoire de ne pas trop dévier de la nomenclature habituelle. * 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'''''. * 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. 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 ''fenêtre d'instruction'', 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'']] ==Les dépendances de données== L'exécution dans le désordre est limitée par plusieurs facteurs, l'un d'entre eux étant les '''dépendances de données'''. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. C'est le cas le plus évident, mais on a aussi des cas moins intuitifs. Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c'est à dire au même registre ou la même adresse mémoire. En tout, cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent la même donnée, mais pas en même temps. * '''Read after write''' : Une instruction a pour opérande le résultat d'une autre : elle lit le résultat écrit par la première. * '''Write after write''' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Pour les dépendances ''Read after read'', on peut mettre les deux instructions dans n'importe quel ordre, cela ne pose aucun problème. Ce ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, ce n'est pas le cas avec les trois autres types de dépendances. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances : * '''Read after write''' : La première instruction doit écrire son résultat avant que la seconde le lise. * '''Write after write''' : La première instruction doit écrire son résultat avant la seconde. * '''Write after read''' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue. : Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Précision importante : les dépendances ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. ===Les dépendances de registre et mémoire=== Pour ce qui est des dépendances de données, les accès mémoire se distinguent des autres instructions. Les instructions mémoire ont des dépendances qui leur sont propres, que les autres instructions n'ont pas. Et cela nous amène à parler des dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité est réservée aux pipelines complexes, qui utilisent une unité mémoire particulière. Nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire, les chapitres qui vont suivre vont se débrouiller avec une unité mémoire "bloquante", qui ne fait qu'un accès mémoire à la fois. Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites. Et autant les registres lus/écrit sont connus dès que l'instruction est décodée, autant les adresses sont souvent calculées à l'exécution ou mémorisées dans des registres. Les deux types de dépendances sont donc le fait de circuits séparés, qui sont dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire et notamment de la file d'écriture, les autres dépendances sont gérées pendant/après l'étape de décodage d'instruction. Aussi, il est important de séparer les deux le plus possible. ===Les dépendances de registre=== Dans ce chapitre, nous allons nous préoccuper des trois dépendances de registres suivantes : * '''''Read after write''''' : Une instruction lit un registre dans laquelle l'instruction précédente a écrit. * '''''Write after read''''' : Une instruction lit un registre pour récupérer une opérande, mais ce registre est ensuite écrasé par une instruction ultérieure. * '''''Write after write''''' : Deux instructions écrivent dans le même registre, mais à des instants différents. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances RAW et WAW, imposent que la première instruction ait écrit son résultat avant que la seconde lise ses opérandes. Si on ne fait pas ça, la seconde instruction lit des opérandes invalides ou voit son écriture écrasée par une écriture précédente. Pour éviter cela, la seconde instruction doit être démarrée quand la première s'est totalement terminée. Bloquer le pipeline est donc une nécessité. Mais diverses optimisations permettent de mitiger l'impact des dépendances de données. Par exemple, dans le prochain chapitre, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. Les dépendances WAW et WAR peuvent aussi être totalement supprimées par des techniques de renommages de registres, qui auront leur chapitre dédié. Intuitivement, on se dit que les dépendances de registres concernent les instructions arithmétiques. Mais les instructions d'accès mémoire et les branchements aussi ont des dépendances de registres. Les instructions d'accès mémoire ont comme opérande une adresse mémoire, et potentiellement d'une donnée à écrire. Et les deux peuvent être récupérée dans les registres, si le mode d'adressage indirect est utilisé. Idem avec l'adressage indicé, sauf que les opérandes servent à calculer l'adresse. L'instruction LOAD a aussi un registre de destination, pour accepter la donnée lue. Ce qui fait deux sources de dépendances de registre : le registre opérande, le registre de destination. Si une instruction de calcul ou une autre instruction mémoire lit/écrit dans ces registres, une dépendance de donnée a lieu. ===Les dépendances présentes dépendent du pipeline=== Il faut préciser que les dépendances WAR et WAW n'apparaissent pas sur certains pipelines. Par exemple, sur un pipeline fixe, les écritures se font dans l'ordre du programme, ce qui implique que les dépendances WAW n'existent pas. Les dépendances WAW, quant à elles, n'apparaissent que sur les pipelines dynamiques, ou alors si les écritures prennent plusieurs étages. Et encore, elles sont "résolues" si le processeur utilise un ROB ou toute autre technique de remise en ordre des écritures. De même, les dépendances d'adresse WAR sont impossibles avec des exceptions précises, car les écritures se font à la fin du pipeline, après les lectures. Et même dans le cas général, les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Pour les processeurs ''in-order'', elles ne se manifestent que dans des pipelines un peu tordues, dans des circonstances où les écritures sont très précoces et les lectures tardives. En pratique, les dépendances WAR n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. {|class="wikitable" |- ! Dépendance ! Pipeline ! colspan="2" | Solutions |- ! RAW | Tous les pipelines | rowspan="3" | Bloquer le pipeline | Contournement |- ! WAW | Pipelines dynamiques, sans remise en ordre des écritures | rowspan="2" | Renommage de registres |- ! WAR | Pipelines dynamiques avec exécution dans le désordre |} Les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency'') et sont 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. 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é. Et on peut les faire disparaitre avec des techniques dites de renommage de registre, qui feront l'objet d'un chapitre ultérieur. Les premiers processeurs MIPS avaient une technique très particulière pour éliminer les dépendances structurelles de ce genre, qui éliminait aussi partiellement les dépendances WAW. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications et éventuellement les divisions. Ils ajoutaient un aval séparé, un simple circuit multiplieur/diviseur séparé de l'ALU entière. Les instructions multi-cycles disposaient de leur propre set de registres rien que pour elles, ce qui résolvait pas mal de problèmes de conflits d'accès aux registres. ==La microarchitecture d'un CPU à exécution dans le désordre== Mine de rien, nous sommes arrivé à un moment important du cours, et faire une sorte de synthèse rapide ne ferait pas de mal. Nous nous sommes assez éloignés d'un processeur avec un pipeline basique. Les modifications les plus importantes sont l'intégration de plusieurs ''back-end'' sur les pipelines dynamiques, l'ajout de techniques de prédiction de branchement, le support des exceptions précises avec un ROB, l'ajout de l'exécution dans le désordre. Le résultat est un processeur tel qu'illustré ci-dessous. Vous voyez que celui-ci est organisé en quatre "sur-circuits", quatre regroupements de circuits aux fonctions bien distinctes. * Le '''''front-end''''' s'occupe du chargement des instructions et de leur décodage. Il regroupe l'unité de chargement, les décodeurs et l'unité de prédiction de branchement, parfois une unité de préchargement d'instructions. * Le ''back-end'' ou '''chemin de données''' correspond au chemin de données, avec les unités de calcul, les registres, mais aussi le réseau de contournement. * L''''unité d'exécution dans le désordre''' regroupe le ROB, le ou les ''schedulers'', et d'autres circuits qu'on n'a pas encore vu à ce stade du cours (lunité de renommage de registres). * Le '''système mémoire''' regroupe les caches de données, le contrôleur mémoire, ainsi que d'autres structures. [[File:MIT 6.823 Computer System Architecture, Fall 2005 - Pentium 4 Block Diagram - 4332479141.jpg|centre|vignette|upright=2.5|Processeur avec exécution dans le désordre.]] ===Le nombre d'instructions en vol augmente=== Maintenant, parlons un peu des ''instructions en vol''. Pour rappel, une instruction en vol est une instruction en cours de traitement dans le pipeline. Sur les processeurs ''in-oder'', il s'agit des µops émises, qui sont soit en train de lire leurs registres, de s'exécuter dans une ALU/FPU/unité mémoire, ou qui attendent de pouvoir enregistrer leurs résultats dans les registres. Sur un processeur OOO, il faut aussi rajouter les µops en attente dans le ''scheduler''. La conséquence est qu'un processeur OOO peut traiter plus d'instructions en vol qu'un processeur ''in-order''. Il est donc plus performant. Mais cela a aussi d'autres conséquences. Une autre conséquence, liée à la précédente, est que la prédiction de branchement devient plus importante. Un processeur ''in-order'' a une pénalité de prédiction assez faible, car il a peu d'instructions en vol. A l'opposé, un processeur OOO a tellement d'instructions en vol que n'en annuler ne serait-ce que la moitié à cause d'une prédiction de branchement demande d'annuler une bonne centaine d'instructions. Les processeurs OOO ont donc des unités de prédiction de branchement bien plus pêchues que leur équivalent ''in order''. ===La présence quasiment systèmatique d'un ROB=== La quasi-totalité des processeurs à exécution dans le désordre utilisent un tampon de réordonnancement. Il permet de gérer les exceptions précises, mais aussi les mauvaises prédictions de branchement et bien d'autres. Il est cependant bien plus gros sur les processeurs OOO que sur les processeurs ''in order''. En effet, il mémorise de quoi annuler les instructions en vol, si une mauvaise prédiction de branchement se produit où quand une exception matérielle survient. Le ROB d'un processeur OOO des années 2020 fait généralement plusieurs centaines d'instructions et approche doucement du millier. En comparaison, un processeur ''in-order'' se débrouille avec un ROB simplifié, qui ne dépasse pas la dizaine d'instructions. 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. ===Les défauts de cache non-bloquants=== Un énorme avantage de l'exécution dans le désordre est que les défauts de cache ne bloquent plus le pipeline. Pour comprendre pourquoi, imaginons qu'une lecture déclenche un défaut de cache. Avec un pipeline ''in-order'', le processeur n'a pas le choix et doit bloquer le pipeline, pour plusieurs raisons. La première est qu'il ne faut pas que des instructions exécutées après la lecture enregistrent leurs résultats dans les registres. A la rigueur, ce problème peut être résolu avec un ROB. Le second problème est les dépendances entre la lecture et d'autres instructions. Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures. Appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Les instructions lecture-dépendantes doivent être retardées tant que le résultat de la lecture n'est pas connu. Avec l'exécution dans le désordre, retarder les instructions lecture-dépendantes se fait naturellement. Le processeur ne démarre celles-ci que quand l'opérande est disponible, donc quand la lecture s'est terminée. L'unité mémoire prévient naturellement quand la donnée lue est disponible, via un signal ''Data Available''. Sur un processeur ''in-order'', la situation est beaucoup plus compliquée. Une implémentation simple bloquerait le pipeline tant que le résultat d'une lecture n'est pas disponible. Mais ce n'est pas du tout ce qui est fait en pratique. A la place, le processeur part du principe que la lecture ne fera pas de défaut de cache. Les instructions sont donc émises en tenant compte de la latence normale du cache L1, en supposant que les lectures seront les plus rapides possibles. En cas de défaut de cache, le pipeline est intégralement gelé. ===Les accès mémoire avec l'exécution dans le désordre=== 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. [[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 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. 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]] La file de µops mémoire a son propre ''scoreboard'', pour savoir si la µops la plus ancienne est prête à être émise. L'usage d'une file de µops implique que la file de mémorise pas les opérandes des µops. L'adresse à lire/écrire est lue après l'émission, idem pour la donnée à écrire pour les écritures. Si l'unité mémoire fait les calculs d'adresse, ce sont les registres opérande qui sont lus. L'unité d'émission mémoire émet l'instruction, puis la µops lit les registres, et l'accès mémoire rentre dans l'unité mémoire et s'exécute. On parle alors de '''lecture après émission'''. De plus, rappelons les écritures doivent être retardées tant que les instructions précédentes ne sont pas terminées. Pour cela, le processeur les mets en attente dans une ''file d'écriture'', qui collabore avec le ROB. 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''. [[File:File de µops mémoire avec calcul d'adresse dans l'unité mémoire.png|centre|vignette|upright=2|File de µops mémoire avec calcul d'adresse dans l'unité mémoire]] Les processeurs modernes peuvent exécuter les accès mémoire dans le désordre, grâce à des techniques de '''''désambiguïsation mémoire'''''. Si elles sont mises en place, 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. L'unité mémoire garde une trace de l'ordre des accès mémoire, elle met en attente les accès mémoire si besoin, elle décide quand les accès mémoire démarrent, etc. En somme, l'unité mémoire dispose de son propre ''scheduler'' intégré. Nous les verrons dans un chapitre dédié, ce qui fait que je ne détaille pas plus que cela. Tout ce qu'il faut retenir est que les µops mémoire sont traités à part des autres µops, pour l'exécution dans le désordre. Il y a le ''scheduler'' et une voie séparée pour les µops mémoire proprement dites. Et ce, peu importe que les accès mémoire soient faits dans l'ordre ou dans le désordre. Sans cela, le processeur ne peut pas fonctionner normalement. Un avantage de séparer les µops mémoire des autres, est que cela permet d'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. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboard et le ROB | prevText=Le scoreboard et le ROB | next=Les schedulers et stations de réservation | nextText=Les schedulers et stations de réservation }} </noinclude> cgqtg07t030yawa1m0tqx12muqc7fgc Fonctionnement d'un ordinateur/Le scoreboard et le ROB 0 65886 775047 775030 2026-10-12T14:52:45Z Mewtow 31375 775047 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur sans exceptions précises. Un tel processeur simplifierait grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, cela ne résoudrait pas tout. Au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précise. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> 7olpxkh5qib38mdektuz7dx9ncompvt 775048 775047 2026-10-12T15:00:53Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775048 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. Bizarrement, les exemples de processeur de ce type ne sont pas nombreux, mais ils le sont plus que les processeurs avec un ''scoreboard'' et un ROB. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> szno64hx40jx1nfqb7v85iay4eb4toj 775049 775048 2026-10-12T15:16:36Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775049 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. Bizarrement, les exemples de processeur de ce type ne sont pas nombreux, mais ils le sont plus que les processeurs avec un ''scoreboard'' et un ROB. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | ALU entière|| ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> ph7f1ajkzkao6ni3mk4qv358eyqfi7v 775050 775049 2026-10-12T15:17:01Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775050 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. Bizarrement, les exemples de processeur de ce type ne sont pas nombreux, mais ils le sont plus que les processeurs avec un ''scoreboard'' et un ROB. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> ksreywa5t4ag77xujphp5x7rr9k33wj 775051 775050 2026-10-12T15:17:25Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775051 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. Bizarrement, les exemples de processeur de ce type ne sont pas nombreux, mais ils le sont plus que les processeurs avec un ''scoreboard'' et un ROB. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> lex59u3lw7q5fflaqhsuas7xhqgapl2 775052 775051 2026-10-12T15:19:12Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775052 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les processeurs Xscale d'Intel=== Bizarrement, les exemples de processeur de ce type ne sont pas nombreux, mais ils le sont plus que les processeurs avec un ''scoreboard'' et un ROB. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> qd7ymz9y8vwhci0k3ddw4ed61e7z8xd 775053 775052 2026-10-12T15:30:47Z Mewtow 31375 /* Annexe : le scoreboard sans exceptions précises */ 775053 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme qui s’applique en absence de branchements. Le mécanisme s'active quand une lecture déclenche un défaut de cache ou toute autre situation. En théorie, le processeur devrait bloquer son pipeline, mais le processeur ne le fait pas. A la place, il continue à exécuter des instructions malgré le défaut de cache, à condition que les instructions ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> ovxnvcv696jzuazex7ktg58goxpi3cz 775092 775053 2026-10-12T21:51:52Z Mewtow 31375 /* La complétion dans le désordre */ 775092 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais de rares optimisations permettent les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> 20l5cqp7addn7j2de79bbx5c1pq6ug4 775094 775092 2026-10-12T21:55:51Z Mewtow 31375 /* La complétion dans le désordre */ Déplacement autre chapitre, plus adapté 775094 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order'' classiques, avec un pipeline dynamique. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' est utilisé pour désigner beaucoup de choses. Il a notamment été utilisé pour désigner le ''scoreboard'' du CDC 6600, mais c'était une implémentation très particulière, qui ne correspond pas du tout aux ''scoreboard'' vus dans ce chapitre. Le CDC 6600 ne gérait même pas les exceptions précises. Mais nous verrons cela dans le chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, mais qui est légèrement simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, mais doit quand même gérer quelques problèmes liés à l'ordre des écritures. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> ekk3lk092rtjv0ma30bd5jy9vdvehqn 775097 775094 2026-10-12T22:15:49Z Mewtow 31375 775097 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order''. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' était initialement utilisé pour désigner le ''scoreboard'' du CDC 6600, dont l'implémentation était très particulière, et qui aura droit à un chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, qui est simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, grâce au système de remise en ordre des écritures, à la fin du pipeline. Les processeurs avec un ''scoreboard'' et des exceptions précises sont assez rares. La plupart se passent du système de remise en ordre complétement ! Le ''scoreboard'' du CDC 6600 et du CDC 7600 en sont les premiers exemples. Mais ils ne sont pas les seuls, et nous dédierons une section entière à de tels processeurs. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> 7fgyxude8kn9y9spf6uokni5g7m7j4t 775113 775097 2026-10-12T23:44:55Z Mewtow 31375 /* Le commit multiple dans le ROB */ 775113 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order''. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' était initialement utilisé pour désigner le ''scoreboard'' du CDC 6600, dont l'implémentation était très particulière, et qui aura droit à un chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, qui est simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, grâce au système de remise en ordre des écritures, à la fin du pipeline. Les processeurs avec un ''scoreboard'' et des exceptions précises sont assez rares. La plupart se passent du système de remise en ordre complétement ! Le ''scoreboard'' du CDC 6600 et du CDC 7600 en sont les premiers exemples. Mais ils ne sont pas les seuls, et nous dédierons une section entière à de tels processeurs. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ==La complétion dans le désordre== L'exécution dans le désordre implique la présence d'un ROB, ou d'une structure similaire. Sans cela, l'exécution dans le désordre aurait des exceptions imprécises, ce qui n'est pas acceptable sur les processeurs commerciaux actuels. Le ROB remet dans l'ordre les écritures dans les registres architecturaux, afin de gérer les branchements et exceptions. Il accumule pour cela les instructions exécutées en avance, pour retarder leurs écritures, afin qu'elles se fassent au bon moment. Mais de rares optimisations permettent de désactiver cette remise en ordre dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. Elles s'activent en absence de branchements et quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les isntructions quittent le ROB en avance. Le résultat est que écritures se font dans le désordre, dans le ''retirement register file'', ou le banc de registres physique. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. L'avantage est que les instructions quittent le ROB plus rapidement que prévu, ce qui utilise sa capacité au mieux. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> 0jxydyxtjcwvkd7mc33jt2uya0qw5kt 775114 775113 2026-10-12T23:45:07Z Mewtow 31375 /* La complétion dans le désordre */ 775114 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order''. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' était initialement utilisé pour désigner le ''scoreboard'' du CDC 6600, dont l'implémentation était très particulière, et qui aura droit à un chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, qui est simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, grâce au système de remise en ordre des écritures, à la fin du pipeline. Les processeurs avec un ''scoreboard'' et des exceptions précises sont assez rares. La plupart se passent du système de remise en ordre complétement ! Le ''scoreboard'' du CDC 6600 et du CDC 7600 en sont les premiers exemples. Mais ils ne sont pas les seuls, et nous dédierons une section entière à de tels processeurs. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== L'exécution dans le désordre implique la présence d'un ROB, ou d'une structure similaire. Sans cela, l'exécution dans le désordre aurait des exceptions imprécises, ce qui n'est pas acceptable sur les processeurs commerciaux actuels. Le ROB remet dans l'ordre les écritures dans les registres architecturaux, afin de gérer les branchements et exceptions. Il accumule pour cela les instructions exécutées en avance, pour retarder leurs écritures, afin qu'elles se fassent au bon moment. Mais de rares optimisations permettent de désactiver cette remise en ordre dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. Elles s'activent en absence de branchements et quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les isntructions quittent le ROB en avance. Le résultat est que écritures se font dans le désordre, dans le ''retirement register file'', ou le banc de registres physique. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. L'avantage est que les instructions quittent le ROB plus rapidement que prévu, ce qui utilise sa capacité au mieux. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> fy467fzu6wdoq4r0cbig73vrstp7b0m 775115 775114 2026-10-12T23:45:29Z Mewtow 31375 /* La complétion dans le désordre */ 775115 wikitext text/x-wiki Le chapitre précédent portait sur les processeurs ''in-order''. Ils sont opposés aux processeurs avec exécution dans le désordre, qui sont justement le sujet de ce chapitre. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. Pourtant, à la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Il peut donc y avoir une '''inversion de l'ordre des écritures''', sous entendu, écritures dans les registres. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Les processeurs ''in-order'' garantissent que des instructions termineront dans l'ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner. Les processeurs à exécution dans le désordre, eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance, mais utilisent un système d'exception précise sous stéroïde pour remettre en ordre les écritures. Et il est temps d'aborder une forme bien précise d'exécution dans le désordre. Les processeurs en question sont appelés des processeurs ''in-order issue - out-of-order commit - in-order writeback''. Okay, j'ai rien dit, on va parler de '''processeurs à ''scoreboard'''''. Les processeurs de ce chapitre ressemblent fortement aux processeurs ''in-order''. Ils n'ont pas de registres d'échelonnement, ils font avec un pipeline dynamique. La différence est que la remise en ordre des écritures se fait à la fin du pipeline, pas au début. Avec un processeur ''in-order'', l'unité d'émission retarde les instructions, pour éviter une inversion de l'ordre des écritures. Mais un processeur à ''scoreboard'' ne le fait pas. Il laisse les instructions calculer leurs résultats dans le désordre. Par contre, ces résultats ne sont pas écrits dans les registres immédiatement. A la place, ils sont mis en attente dans une structure quelconque, qui fait les écritures dans l'ordre. [[File:Processeur avec un scoreboard.png|centre|vignette|upright=2.5|Processeur avec un scoreboard]] L'unité d'émission est appelée un '''''scoreboard''''', avec cependant un petit détail terminologique. Le terme ''scoreboard'' était initialement utilisé pour désigner le ''scoreboard'' du CDC 6600, dont l'implémentation était très particulière, et qui aura droit à un chapitre dédié. Pour ce chapitre, un ''scoreboard'' est une unité d'émission, qui est simplifiée comparé à celle du chapitre précédent. L'unité du chapitre précédent a trois fonctions : détecter les dépendances structurelles, détecter les dépendances RAW, remettre en ordre les écritures. Un ''scoreboard'' se passe de la troisième fonction, grâce au système de remise en ordre des écritures, à la fin du pipeline. Les processeurs avec un ''scoreboard'' et des exceptions précises sont assez rares. La plupart se passent du système de remise en ordre complétement ! Le ''scoreboard'' du CDC 6600 et du CDC 7600 en sont les premiers exemples. Mais ils ne sont pas les seuls, et nous dédierons une section entière à de tels processeurs. ==L'inversion de l'ordre des écritures== L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle... Pour conserver l'ordre des écritures, un processeur ''in-order'' ''retarde l'exécution de certaines instructions avec des dépendances''. Nous avions vu cela dans le chapitre sur les processeur ''in-order'', avec le ''result shift register'', je ne reviens pas dessus. Les processeur avec un ''scoreboard'' utilisent une autre solution : exécuter les instructions en avance, mais '''retarder les écritures dans les registres''' à la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. Une solution similaire, déjà abordée dans les chapitres sur les pipelines fixes, utilise des registres d'échelonnement. Mais un défaut est que cette solution utilise un pipeline fixe. Impossible d'avoir un pipeline dynamique, ni d'avoir des défauts de cache non-bloquants. Les techniques qu'on va voir dans ce qui suit utilise une version sous stéroïde de ces registres d’échelonnement, qui sont compatibles avec un pipeline dynamique, non-fixe. Ils permettent d'avoir des exceptions précises, alors que les instructions terminent dans le désordre. ===Le tampon de réordonnancement=== Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit. La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe. [[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]] Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats. Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc. [[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]] Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée. Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises. {|class="wikitable" |+ Une entrée du ROB |- | ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception |} Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans. Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement. ===Le tampon d’historique=== Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB). Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception. [[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]] ===Le banc de registres futurs=== Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc. Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit. Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct. Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.* [[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]] Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres. ===Les point de contrôle de registre=== La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements. Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur. Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde. [[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]] Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc. La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF. ==La optimisations liées au ROB== Dans le reste du cours, nous allons supposer que le processeur utilise un ROB. Les autres techniques sont tellement peu utilisées que ca ne sert à rien d'en parler. Tous les processeurs "haute performance" modernes ont un ROB, les exceptions sont très rares. Le ROB peut être la source de plusieurs optimisations assez intéressantes. Il est possible de lui ajouter plusieurs ports d'écriture, de le connecter au réseau de contournement, etc. Et nous allons voir ces optimisations dans ce qui suit. ===Le contournement avec un ROB=== Dans le chapitre sur les pipelines fixes, nous avons abordé la technique du contournement, qui permet d'utiliser un résultat avant qu'il soit enregistré dans les registres. Pour cela, la sortie de l'unité de calcul était envoyée sur son entrée, grâce à un système impliquant des multiplexeurs. La remise en ordre des écritures ajoute une mémoire pour remettre en ordre les écritures dans les registres. Et l'on peut l'adapter pour gérer le contournement. L'idée est de lire les opérandes depuis la mémoire qui remet en ordre les écritures. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi. Le ROB peut aussi être adapté pour gérer le contournement. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail. Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes. [[File:Pipeline avec un ROB.png|centre|vignette|upright=2|Pipeline avec un ROB.]] Un processeur avec un ''scoreboard'' et un ROB exécute les instructions plus tôt et permet d'utiliser leur résultat en avance. Et c'est un avantage du ROB sur les registres d’échelonnement : les résultats sont disponibles plus tôt, via le réseau de contournement. Les dépendances RAW ont donc moins d'impact. ===Le ''commit'' multiple dans le ROB=== Les instructions écrivent leur résultat dans le ROB, qui les remet dans l'ordre si besoin. Vu que les instructions enregistrent leurs résultats dans le désordre, il peut arriver que deux instructions veuillent enregistrer leur résultat dans le ROB, en même temps. Par exemple, imaginons que l'on exécute une multiplication de 3 cycles, suivie au cycle suivant par une opération qui n'en prend que 2. Elles tenteront d'enregistrer leurs résultats en même temps 3 cycles après pour la première, 1 + 2 cycles pour l'autre. Elles voudront donc enregistrer un résultat en même temps. Et il faut gérer la situation. La solution la plus simple est que l'unité d'émission détecte ce genre de conflit d'accès au ROB, et retarde la seconde instruction d'un cycle pour résoudre le problème. Pour cela, elle utilise un ''result shift register'' modifié, qui mémorise si à tel cycle du pipeline, le ROB recevra une écriture. Il permet de réserver le port du ROB pour un cycle bien précis. Concrètement, prenons une instruction met N cycles à s’exécuter, qui fournit un résultat à enregistrer au cycle N. Lorsqu'elle entre dans l'unité d'émission, le énième bit du ''result shift register'' est vérifié. S'il vaut zéro, alors le ROB est inutilisé pendant ce cycle. L'instruction réserve alors ce cycle pour écrire dans le ROB, en mettant le bit à 1. Et uniquement ce bit, pas les précédents, petite nuance comparé au ''result shift register'' vu plus haut. S'il est déjà à 1, le port d’écriture du ROB a été réservé pour ce cycle, la micro-opération est mise en attente. {|class="wikitable" |+ Result Shift Register, réservation du ROB |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 |- | 0 || 1 || 0 || 1 || 1 || 0 || 1 |} Une autre solution ajoute des ports d'écriture sur le ROB, afin qu'il puisse recevoir plusieurs résultats par cycle. Il y a typiquement un port d'écriture par unité de calcul, sur le ROB. La première solution a un cout en performance, mais est simple à implémenter et son cout en transistor est limité. Pour la seconde solution, son cout en transistor est important, sans compter que le ROB devient alors plus gourmand en énergie. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul. ===La complétion dans le désordre=== Le ROB remet dans l'ordre les écritures dans les registres architecturaux, afin de gérer les branchements et exceptions. Il accumule pour cela les instructions exécutées en avance, pour retarder leurs écritures, afin qu'elles se fassent au bon moment. Mais de rares optimisations permettent de désactiver cette remise en ordre dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. Elles s'activent en absence de branchements et quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les isntructions quittent le ROB en avance. Le résultat est que écritures se font dans le désordre, dans le ''retirement register file'', ou le banc de registres physique. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. L'avantage est que les instructions quittent le ROB plus rapidement que prévu, ce qui utilise sa capacité au mieux. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. ==Les défauts de cache avec un ''scoreboard''== Les processeurs avec un ROB peuvent implémenter diverses optimisations, l'une d'entre elle étant les '''défauts de cache non-bloquants'''. Le terme décrit assez bien cette optimisation : les défauts de cache ne bloquent plus le pipeline. Le processeur peut continuer à exécuter des instructions dans l'ALU entière, pendant que le défaut de cache est géré par l'unité mémoire. Et ce n'est pas la seule optimisation, les processeurs avec un ''scoreboard'' peuvent implémenter de nombreuses optimisations liées aux défauts de cache, comme un ''replay system''. ===Les défauts de cache non-bloquants=== Un processeur ''in-order'' bloque le pipeline lors d'un défaut de cache, car il n'a pas le choix. Il se peut que le défaut de prenne beaucoup de temps à être traité, facilement plusieurs centaines de cycles d'horloges s'il doit lire la donnée en mémoire RAM, encore plus si le défaut de cache se double d'un défaut de page (en mémoire virtuelle). Et pendant ce temps, s'il continuait à exécuter des instructions, celles-ci sortiraient du pipeline et enregistreraient leurs résultats dans les registres. Si la lecture lève une exception, ces résultats ne peuvent pas être annulés. Mais avec un ROB, tout change. Le processeur peut continuer à exécuter des instructions après un défaut de cache. Si le processeur a un ROB suffisamment grand, il peut continuer à exécuter des instructions après un défaut de cache. Mais les instructions en question ne quitteront pas le pipeline. Elles seront mises en attente dans le ROB. L'enregistrement du résultats dans les registres est retardé tant que le défaut de cache n'est pas résolu. Pour compenser, les résultats en attente dans le ROB sont accessibles par les autres instructions via le réseau de contournement. Un autre problème est la gestion des instructions dépendantes de la lecture. Et c'est une autre raison qui force les processeurs ''in-order'' à bloquer leur pipeline en cas de défaut de cache. Imaginez qu'une instruction utilise une donnée lue par une lecture précédente. Si on ne bloquait pas le pipeline, l'instruction se propagerait dans le pipeline normalement, jusqu'à lire le registre de destination de la lecture. Mais si la donnée n'est pas encore arrivée, à cause du défaut de cache, la donnée lue sera invalide. Si une instruction en vol a une dépendance avec la lecture, pas le choix : il faut geler le pipeline. Et ce avec ou sans ROB. La présence d'un ROB n'est pas absolument nécessaire pour cette optimisation. Nous avions vu dans le chapitre précédent que les processeurs ARM Cortex A53, A55 et A510, intégraient de tels ''défauts de cache non-bloquants'', mais seulement pour les lectures. Mais sans ROB, ils ne pouvaient exécuter en avance qu'une dizaine d'instruction, guère plus. Il y avait maximum une instruction par étage de pipeline, la lecture était en quelque sorte bloquée dans l'étage de ''writeback'' et les instructions s'accumulaient dans les étages précédent. Mais avec un ROB, on peut aller plus loin, et mettre en attente une bonne centaine d'instructions. Sachant qu'un défaut de cache L1 peut prendre entre 10 et 100 cycles d'horloge, avec des pointes à plusieurs milliers en cas de lecture RAM ou de défaut de page, ce n'est pas du luxe. Ainsi, un processeur avec ROB a un avantage certain, ce qui fait que tous les processeurs modernes en utilisent un. ===L'émission anticipée des accès mémoire=== Non seulement les processeurs avec un ''scoreboard'' ne se bloquent pas lors d'un défaut de cache, mais ils peuvent en plus en profiter pour gérer le contournement de l'unité mémoire. Pour rappel, l'idée est qu'une instruction peut lire en avance une donnée lue. La lecture est disponible en sortie de l'unité mémoire, avant d'être écrite dans les registres. Elle est disponible via le réseau de contournement. Pour profiter du contournement, on doit émettre une instruction en avance, pour qu'elle arrive en entrée de l'ALU pile quand l’opérande est disponible. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Et cela pose des problèmes pour l'émission anticipée et le contournement en général. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Corriger la situation peut impliquer le ROB : les instructions sont marquées comme invalides, elles n'enregistrent pas leur résultat dans les registres, et les instructions suivantes le sont aussi. Un défaut de cache déclenche une exception matérielle, qui est traitée par le système d'exception précises, comme un ROB. Une solution alternative gèle l'instruction lecture dépendante en entrée de l'unité de calcul. La micro-opération émise en avance attente que l'opérande soit disponible, que le défaut de cache se passe. L'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Une fois le calcul terminé, elle écrira son résultat dans le ROB et le tout sera remis en ordre. Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités. Une dernière solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre. : Détail qui a son importance : un processeur ''in-order'' fait naturellement de l'émission anticipée, pour les lectures. Simplement, en cas de défaut de cache, le pipeline est totalement bloqué. ===Le ''replay pipeline'' du Pentium 4=== Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande. L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ». [[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]] Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire. Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''. : La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance. [[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]] Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente. Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple). [[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]] Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps. [[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]] On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission. ==Annexe : le ''scoreboard'' sans exceptions précises== Il est possible d'imaginer ce que donnerait un processeur avec ''scoreboard'' sans exceptions précises. L'absence d’exceptions précises simplifie grandement le design du processeur, on n'aurait pas besoin de ROB ou de tampon d'historique, de banc de registre futur, etc. Par contre, au-delà des problèmes liés aux exceptions précises, l'inversion de l'ordre des écritures pose d'autres problèmes, comme on le verra plus bas. ===Les exemples réels de processeurs avec un ''scoreboard''=== Bizarrement, les exemples de processeurs de ce type sont plus nombreux, que les processeurs avec un ''scoreboard'' et un ROB. La raison est qu'un ''scoreboard'' est utilisé pour implémenter une forme limitée d'exécution dans le désordre, qui est assez économe en circuits. Et le ROB est lui assez gourmand en circuits, ce qui ne colle pas avec cet objectif d'économie. Un concepteur de processeur qui a un budget en transistors limité préférera utiliser un ''scoreboard'' sans exceptions précises. A l'opposé, un concepteurs de processeur avec un budget en transistor conséquent préférera utiliser de la vraie exécution dans le désordre telle qu'on l'abordera dans le chapitre suivant, avec un ROB pour gérer les exceptions précises. Les processeurs avec un ''scoreboard'' et sans ROB sont donc rares. Parmi les CPUs, ils sont presque inexistants, du fait de l'absence d'exceptions précises. Les seules utilisations se trouvent dans les systèmes embarqués, et même là, ils sont rares. Une autre utilisation est celle des processeurs de ''shaders'' des GPU modernes. Ils implémentent tous un ''scoreboard'' amélioré, sous stéroïde, sans système d'exceptions précises. Les GPU ne permettent pas d'interrompre l'exécution d'un programme (un ''shader'') tant qu'il ne se termine pas de lui-même, sauf à RESET le processeur de ''shader''. Mais ils sont très particuliers et je préfère laisser leur étude à mon wikilivre sur les cartes graphiques. Un exemple possible est celui des processeurs Xscale d'Intel, qui implémentaient le jeu d’instruction ARM v5, avec cependant l'absence d'instructions flottantes. Si j'en crois le manuel d'optimisation officiel (''Intel® XScale™ Core Optimization Guide'', section A.2), ils utilisaient bien un ''scoreboard'', mais sans système pour remettre en ordre les écritures. Ces processeurs étaient prévus pour une utilisation embarquée, et ont été vendus sous dans du matériel réseau (routeurs, ''network processors''), et dans quelques''System On Chip''. Les processeurs Xscale avaient deux voies : une voie regroupant une ALU entière et une unité mémoire, et une voie se résumant à une unité de calcul MAC (''Multiply And Accumulate'') qui travaillait sur des nombres entiers. L'usage d'un ''scoreboard'' avec deux/trois voies était relativement simple. Le ''scoreboard'' permettait d'exécuter des instructions dans l'ALU entière en parallèle d'un accès mémoire, mais aussi d'exécuter une opération MAX en parallèle. Les instructions MAC étaient des instructions multi-cycles et l'unité MAC n'était pas pipeliné, alors que l'unité mémoire et l'ALU entière l'étaient. {|class="wikitable" |- ! rowspan="4" | Fetch 1 !! rowspan="4" | Fetch 2 !! rowspan="4" | Decode !! rowspan="4" | Read Operand | rowspan="2" | ALU entière/Calcul adresse || Accès cache L1 || Accès cache L1 || ''Writeback'' |- | Gestion registre état + autres || ''Writeback'' |- | colspan="8" | |- | MAC || MAC || MAC (facultatif) || MAC (facultatif) || ''Writeback'' |} ===Les dépendances ''Write After Write''=== Plus haut, j'ai sous-entendu que l’absence d'exceptions précises avait des effets largement au-delà des problèmes liés aux exceptions et à la prédiction de branchement. Le ROB remet en ordre les écritures, afin d'éviter tout phénomène d'inversion des écritures. Et l'inversion de l'ordre des écritures n'impacte pas que les exceptions précises, mais peut casser complétement l'exécution d'un programme. Prenons deux instructions proches, qui écrivent dans le même registre. Si deux écritures se font dans des registres différents, inverser leur ordre ne concerne que le système d'exceptions précises. Mais si les deux écritures se font dans le même registre, il y a un problème : un registre a été écrasé avec une valeur antérieure, sa valeur est incorrecte. Le problème en question correspond une '''dépendance de donnée de type ''Write After Write''''', ou ''dépendance WAW''. Le terme ''dépendance de donnée'' indique que les deux instructions ne sont pas indépendantes, le terme ''Write After Write'' indique que deux écritures doivent se faire dans un ordre précis. Sans ROB,les dépendances WAW sont gérées par le ''scoreboard'', pas par le système d'exception précises. La détection des dépendances WAW est assez complexe. Une solution simple, mais terriblement peu pratique, compare les registres de destination, sans tenir compte des timings des instructions. L'unité d'émission compare le registre de destination des instructions en vol, avec celui de la micro-opération à émettre. S'il y a correspondance, c'est potentiellement une dépendance WAW. Je dis potentiellement, car la détection est très conservatrice, elle ne cherche pas à savoir s'il y a réelement une dépendance. Elle en tient pas en compte la durée des instructions, de quand ont lieu les écritures. Une implémentation plus précise détecte les dépendances réelles. Elle réutilise le ''result shift register'', qui permet d'interdire une écriture pendant N cycles d'horloge consécutifs. Sauf que cette fois-ci, il y a un ''result shift register'' par registre. Une instruction qui compte écrire dans un registre après N cycles réserve un registre pendant N cycles. L'unité d'émission configure le ''result shift register'' adéquat pour cela. Le registre est alors inaccessible en écriture pendant N cycles. Si une instruction ultérieure tente de le faire, c'est signe qu'il y a une dépendance WAW. L'instruction candidate est alors bloquée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les pipelines in-order | prevText=Les pipelines in-order | next=L'exécution dans le désordre | nextText=L'exécution dans le désordre }} </noinclude> f2gszfsxwb8d0eg3atraornvdw6fkzy Fonctionnement d'un ordinateur/Le pipeline 0 65892 775063 775000 2026-10-12T17:35:17Z Mewtow 31375 /* L'optimisation du pipeline de données */ 775063 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d'execution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==L'optimisation du pipeline de données== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. ===Une solution naïve : se caler sur l'étage limitant=== Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres. Voyons ce que cela implique. Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="3" | Accès mémoire L1 !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> msp4nqiya622a2vo45w1sx8kl56kxhk 775064 775063 2026-10-12T17:36:14Z Mewtow 31375 /* L'accès aux caches L1 est lent, mais se pipeline */ 775064 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d'execution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==L'optimisation du pipeline de données== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. ===Une solution naïve : se caler sur l'étage limitant=== Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres. Voyons ce que cela implique. Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> pn0l0hn1fzkw1xpmurbxaba8k8qx9xe 775080 775064 2026-10-12T19:42:07Z Mewtow 31375 /* L'optimisation du pipeline de données */ 775080 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d'execution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''superpepelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 2wrag31l9kl1duymju0zq5l37fr7w9v 775081 775080 2026-10-12T19:43:56Z Mewtow 31375 /* Les superpepelines : des pipelines subdivisés */ 775081 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d'execution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''superpepelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> ixxdyhfr6mc54ketx69i0p8f2e8mmr4 775082 775081 2026-10-12T19:45:22Z Mewtow 31375 /* Les superpepelines : des pipelines subdivisés */ 775082 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d'execution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 0gkhfxi2ionzz4snvk41ova8ndh19ry 775098 775082 2026-10-12T22:41:50Z Mewtow 31375 /* Le pipeline RISC classique et ses variantes */ 775098 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Et c'est là l'origine du fait que le pipeline a un étage mémoire dédié, séparé de l'étage d'exécution. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 9ohjmpwta9ps04gvulaaijld5ux1pxu 775099 775098 2026-10-12T22:44:23Z Mewtow 31375 /* Le pipeline RISC classique et ses variantes */ 775099 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne sont pas réalisé dans une unité de calcul d'adresse séparée, mais dans l'ALU entière. Et c'est pour cela qu'il utilise un étage d'exécution et un étage mémoire séparé. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire, qui est nettement plus simple à comprendre. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. ===La lecture des opérandes se fait en parallèle du décodage=== Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 7vtuqz0kcl1d6vj59zbl9ciyjon5az2 775100 775099 2026-10-12T22:44:33Z Mewtow 31375 /* Le pipeline RISC classique et ses variantes */ 775100 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne sont pas réalisé dans une unité de calcul d'adresse séparée, mais dans l'ALU entière. Et c'est pour cela qu'il utilise un étage d'exécution et un étage mémoire séparé. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire, qui est nettement plus simple à comprendre. ===La lecture des opérandes se fait en parallèle du décodage=== L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 0dxg318xohib85m31wf7aevhnn5ltd9 775101 775100 2026-10-12T22:46:40Z Mewtow 31375 /* La lecture des opérandes se fait en parallèle du décodage */ 775101 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne sont pas réalisé dans une unité de calcul d'adresse séparée, mais dans l'ALU entière. Et c'est pour cela qu'il utilise un étage d'exécution et un étage mémoire séparé. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire, qui est nettement plus simple à comprendre. ===La lecture des opérandes se fait en parallèle du décodage=== L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Le décodage devient donc beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> nnqnum6ti8ykeaa7r0w0d3cxom5eh41 775102 775101 2026-10-12T22:48:31Z Mewtow 31375 /* Le pipeline RISC classique et ses variantes */ 775102 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne sont pas réalisé dans une unité de calcul d'adresse séparée, mais dans l'ALU entière. Et c'est pour cela qu'il utilise un étage d'exécution et un étage mémoire séparé. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire, qui est nettement plus simple à comprendre. Bizarrement, ce pipeline n'utilise pas l'ALU entière pour faire les branchements. Elle utilise à la place des additionneurs séparés de l'ALU. ===La lecture des opérandes se fait en parallèle du décodage=== L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Le décodage devient donc beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> 4iznnm150mpne1tmpl7v4etydhjy1oy 775103 775102 2026-10-12T22:53:45Z Mewtow 31375 /* Les branchements, leur exécution et leur décodage */ 775103 wikitext text/x-wiki Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec. ==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !== Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc. Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage. [[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]] Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1. [[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]] Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales). [[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]] Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur : [[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]] Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours. ===Les étages du pipeline=== Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus. {|class="wikitable" |- ! Processeur !! Longueur du pipeline |- | Motorola PowerPC G4 || 7 |- | MIPS R4400 || 8 |- | Intel Itanium || 10 |- | Intel Pentium II || 14 |- | Intel Pentium 3 || 10 |- | Intel Pentium 4 ||20 |- | Intel Pentium 4 Prescott ||31 |- | AMD Athlon ||12 |- | AMD Athlon 64 ||16 |- | IBM PowerPC 970 ||16 |- | Sun UltraSPARC IV ||14 |} Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture. Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuter une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent. ===Les pipelines synchrones et asynchrones=== Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant. La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone. [[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]] Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents. [[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]] Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C. [[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]] Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres. ==La séparation des étages de pipeline== Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard. ===Le pipeline Fetch-Exec=== En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC. [[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]] Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons. La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée. Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur. ===Le pipelining du séquenceur=== Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant : * ''Fetch'' : chargement de l'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''. L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple. [[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]] Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant : * ''PC Update'' : mise à jour du ''program counter'' ; * ''Fetch'' : accès au cache d'instruction ; * ''Decode'' : décodage de l'instruction ; * ''Exec'' : exécute l'instruction. Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''. [[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]] Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées. ===Le pipelining du chemin de données=== Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux. L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages : * Chargement d’opérandes : lecture des opérandes depuis les registres. * Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire. * Enregistrement : écriture du résultat dans les registres. [[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]] Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire. Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances. [[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]] Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples. Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci. [[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]] Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage. Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention. Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire. Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes : * ''Read Operands'' (RO) : lecture des opérandes dans les registres ; * ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ; * ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ; * ''Writeback'' (WB) : enregistrement d'une donnée dans les registres. [[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]] Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant. Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile ! Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux. ===La propagation des signaux de commande dans le pipeline=== Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages. [[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]] Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire. [[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]] ==Le pipeline RISC classique et ses variantes== Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique. Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres. Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Les 5 étages sont les suivants : * ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ; * ''Instruction Decode'' (ID) pour l'étage de décodage ; * ''Execution'' (EXEC) pour l'étage d’exécution ; * ''Memory'' (MEM) pour l'étage d'accès mémoire ; * ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ; Il est conçu pour une architecture RISC, avec un jeu d'instruction adapté pour simplifier le pipeline. Déjà, le processeur ne supporte pas les instructions multi-cycles, ce qui signifie que toutes les instructions se font en un cycle d'horloge. Exit donc les multiplications et divisions, qui sont absentes de ce processeur. Et c'est une simplification appréciable, car supporter des instructions multi-cycles complexifie grandement le pipelines. D'ailleurs, dans les deux-trois chapitres qui vont suivre, on mettra les instructions multi-cycles sous le tapis, par souci pédagogique. Par contre, malgré ses simplifications, il gère les modes d'adressage indicés, et d'autres modes d'adressages qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne sont pas réalisé dans une unité de calcul d'adresse séparée, mais dans l'ALU entière. Et c'est pour cela qu'il utilise un étage d'exécution et un étage mémoire séparé. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire, qui est nettement plus simple à comprendre. Bizarrement, ce pipeline n'utilise pas l'ALU entière pour faire les branchements. Elle utilise à la place des additionneurs séparés de l'ALU. ===La lecture des opérandes se fait en parallèle du décodage=== L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline. Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici. Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Le décodage devient donc beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage. Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas. ===Les branchements, leur exécution et leur décodage=== L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe. Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent. [[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]] Mais le pipeline RISC classique exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique. [[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]] Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière. ==Les ''super-pipelines'' : des pipelines subdivisés== Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire. Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 16 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants. Cependant, ce n'est pas toujours le cas, il arrive aussi que les étages limitants soient l'étage de décodage, ou l'étage d'exécution. La solution la plus simple est de '''se caler sur l'étage limitant''', quitte à réorganiser le pipeline. Pour se caler sur l'étage limitant, il est possible de fusionner des étages vraiment rapides. Par exemple, imaginons que l'étage limitant fait 50 µs, qu'un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15. Il est possible de fusionner lecture des opérandes et exécution dans un seul étage. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant. L'inconvénient est que cela réduit le nombre d'étages, et donc le nombre d'instructions en vol. [[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]] Mais il est possible de pipeliner encore plus le processeur, sans se caler sur l'étage limitant. L'idée est de subdiviser l'étage d'accès au cache, l'étage de décodage, voire l'étage d’exécution. Par exemple, l'étage de décodage devient alors deux étages séparés, voire trois. La technique porte le nom de '''super-pipeline'''. Elle permet d'augmenter le nombre d'étages, sans foncièrement changer les circuits du processeur. Il suffit de mettre des registres de pipeline au bon endroit dans le décodeur, l'ALU entière, les caches L1. En général, un super-pipeline a environ une dizaine d'étages, parfois plus, alors qu'un pipeline "classique" a moins de 7-8 étages. ===L'accès aux caches L1 est lent, mais se pipeline=== Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment pipeliner le cache dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Mais pour le moment, nous allons donner un exemple assez basique. On suppose que le cache est un ''phased'' cache, où les ''tags'' et les données sont dans deux SRAM séparées. Un tel cache peut être pipeliné pour avoir un accès en deux cycles : le premier cycle vérifie les ''tags'', le second accède aux données. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Un exemple classique est celui des premiers processeurs MIPS, précisément le MIPS R4000. Son pipeline était un pipeline RISC classique, dans lequel les étages FETCH et MEM étaient pipelinés. Concrétement, le pipeline du MIPS R4000 utilisait deux étages pour lire une instruction dans le cache d’instruction, trois pour lire dans le cache L1 de données. le résultat était un pipeline à 8 étages, qui est décrit dans le tableau ci-dessous. {|class="wikitable" |- ! colspan="2" | Instruction fetch !! Decocage !! Execution !! colspan="2" | Accès cache L1 !! Vérification TLB !! ''Writeback'' |- | ''Fetch'' 1 || ''Fetch'' 2 || ''Decode'' + ''Read Operands'' || ''Exec'' || ''Data Fetch'' 1 || ''Data Fetch'' 1 || ''Tag Check'' || ''Writeback'' |} Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir. ===Les décodeurs sont lents sur les CPU CISC, mais ils se pipelinent=== L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit. : Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres. Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions. Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions. ===Les instructions multi-cycles et l'ALU entière=== Enfin, il est possible de pipeliner l'unité de calcul. Mais nous n'allons pas le faire de façon naïve, nous allons ruser pour gérer des instructions comme les multiplications et les divisions assez simplement. Et comprendre pourquoi, faisons une remarque : les pipelines précédents ont un étage d'exécution unique, ce qui implique que toutes les instructions se font en un seul cycle d'horloge. Plus précisément, un calcul dans l'ALU se fait en un cycle d’horloge, on ne compte pas la lecture et l’écriture des registres. Mais autant c'est bien adapté pour les additions/soustractions, décalages et comparaisons, autant ce n'est pas le cas pour les multiplications et les divisions. Idéalement, il faudrait ajouter de quoi supporter des '''instructions multi-cycles''' au pipeline, à savoir des instructions qui mettent plusieurs cycles pour s'exécuter. Et justement, pipeliner l'unité de calcul permet de supporter ces instructions très facilement. Les instructions multi-cycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Mais cela ne suffit pas, il faut modifier plus profondément le pipeline processeur. Il y a plusieurs implémentations possibles, mais nous n'allons pas toutes les voir. Deux chapitres entiers seront dédiés à cette problématique des instructions multi-cycles. L'implémentation la plus simple utilise plusieurs étages d'exécution consécutifs. Toutes les instructions mettent alors le même nombre de cycles pour s’exécuter. Et là, pas le choix : on doit se caler sur l'instruction la plus lente. Par exemple, si le processeur a des additions qui se font en un cycle, des décalages qui se font en deux, et des multiplications qui se font en 8 cycles, alors il y aura 8 étages d’exécution. En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] De plus, pour avoir un fonctionnement en pipeline, les multiplieurs et FPU doivent être pipelinés. Par exemple, si une multiplication fait 4 cycles, le multiplieur doit être segmenté en 4 étages, chacun effectuant une partie de la multiplication. Rien de compliqué à cela. Par exemple, prenons un multiplieur 32 bits. Il effectue 32 additions successives, on peut le découper en quatre circuits qui font chacun 32/4 = 8 additions. Pipeliner la FPU est un peu plus compliqué, mais rien d'insurmontable. [[File:Exemple avec une unité de multiplication totalement échelonnée.png|centre|vignette|upright=2|Multiplieur échelonné.]] L'implémentation en question donne un pipeline simple. Mais il y a d'autres implémentations bien plus complexes, qui ont des couts en hardware différents. Pour les prochains chapitres, on va supposer que le processeur utilise un pipeline de ce type. Mais une fois dans le chapitre sur les pipelines ''in-order'', nous verrons que certains processeurs ne pipelinent pas les unités de multiplication, ni la FPU. Nous verrons aussi que qu'il y a des pipelines de longueur variables, où le nombre d'étages change suivant l'instruction. Mais laissons cela pour plus tard. ==La performance d'un pipeline== Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle. ===La performance théorique d'un pipeline idéal (approche simplifiée)=== Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages. Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent. [[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]] Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient : [[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]] Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de : : <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline). à un processeur de fréquence : : <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages. On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages. Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge. Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur : : <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter. Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ? Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente. ===La performance théorique d'un pipeline avec des registres inter-étages=== En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient : : <math>t_\text{étage} = \frac{t}{N} + \tau</math> En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline. : <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math> La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne : : <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math> L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres. On peut calculer le gain en comparant la fréquence avec et sans pipeline : : <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math> Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux. ===Le monde réel : l'exemple des processeurs Intel=== Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence. Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient : [[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]] Et comparons la longueur de leur pipeline : {|class="wikitable" |- ! Processeur ! Longueur du pipeline |- | 286 | rowspan="4" | 5 |- | 386 |- | 486 |- | Pentium |- | Pentium 2/3 | 14 |- | Pentium 4 | 31 à 20 selon les modèles |} On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5. Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo. ==Les exceptions précises== Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline. On dit que les exceptions sont ''imprécises''. [[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]] Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après. En soi, le processeur peut très bien ne rien faire contre ce problème, et utiliser des exceptions imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des '''exceptions précises'''. ===Le hardware pour les exceptions précises=== Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline. Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives. [[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]] Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement. Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Elle permet de ne pas bloquer le pipeline au moment même où un défaut de cache a lieu, mais retarde le blocage. L'idée est qu'un défaut de cache lève une exception matérielle, nommée ''cache fault''. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline. Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, comme on le verra dans le chapitre suivant. ===Les écritures mémoire sont retardées avec des exceptions précises=== Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final. Les écritures mémoire sont donc retardées jusqu'à l'étage de ''Writeback''. La mise en attente se fait grâce à une mémoire spécialisée, appelée la '''file d'écriture'''. Le terme "file" trahit le fait que c'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' la met en attente. A l'étage de ''Writeback'', la file d'écriture exécute l'écriture dsans le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées. [[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]] Avec une file d'écriture, l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport, avec un port de lecture séparé du port d'écriture. Le port de lecture est utilisé par l'étage MEM, alors que le port d'écriture est utilisé par l'étage de ''Writeback''. : La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes. La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées. {|class="wikitable" |- ! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback'' |- | MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback'' |- ! colspan="8" | File d'écriture |- | Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8 |} Pour résumer, : <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math> ===Le contournement de la file d'écriture=== Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Lire une donnée qu'on vient d'écrire n'est pas si rare, surtout sur les processeurs x86, qui ont peu de registres, et doivent parfois faire de la place pendant quelques cycles, avant de rapatrier des données dans les registres. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide. Mais il y a une solution assez évidente. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''. [[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le Translation Lookaside Buffer | prevText=Le Translation Lookaside Buffer | next=La prédiction de branchement | nextText=La prédiction de branchement }} </noinclude> mu4vfjsz6v0khjudot6z48reyrebeav Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order 0 65956 775079 774417 2026-10-12T19:30:35Z Mewtow 31375 /* La double émission INT-FLOAT-MEM */ 775079 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 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 très grosse majorité des CPUs double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une 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 || |- ! ... || ... || ... || ... || ... || ... || ... |} De nos jours, même les processeurs superscalaires utilisent l'exécution dans le désordre, et sont tous superscalaires. Et ce même pour les processeurs basse consommation. La loi de Moore nous permet d'utiliser un budget en transistor permettant une exécution dans le désordre minimale, qui améliore grandement les performances pour une consommation en énergie très faible. Les processeurs superscalaires utilisent donc tous l’exécution dans le désordre. Tous ? Non ! Une poignée d'irréductibles processeurs ARM résistent encore et toujours à la tendance globale. ARM a une gamme de processeurs superscalaires sans exécution dans le désordre, qui sont séparés de leurs autres processeurs superscalaires plus puissants. Il s'agit des processeurs ARM Cortex A53, A55, et A510. Il s'agit de processeurs double ou triple émission, prévu pour des applications embarquées, qui privilégient la très basse consommation sur les performances. Ils étaient utilisés dans des ''System On Chip'' assez divers, où ils étaient soit utilisés seuls, soit couplés à des cœurs plus puissants et avec une microarchitecture plus évoluée. ==Le ''front-end'' des processeurs superscalaires ''in-order''== 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 depuis le cache d'instruction (ou depuis la mémoire RAM). Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc 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, les instructions d'un bloc sont chargées, décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après un branchement pris 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 blocs d'instructions doivent être alignées sur 128 bits. La moindre instruction à cheval sur deux blocs posera une erreur d'alignement, que le CPU devrait gérer. Et on peut généraliser à n'importe quelle taille de bloc. Il arrivera qu'une instruction est à 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 superscalaires utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est 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 ''in-order''== 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’appariement 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. 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 pour un multiplieur et un ''barrel shifter'' 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 |} Précisons cependant que quelques processeurs offrent un port dédié pour le multiplieur entier. Les processeurs ARM haute performance sont souvent dans ce cas, avec un port d'émission dédié au multiplieur entier. Mais ils sont l'exception qui confirme la règle. 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 dupliqué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 micro-architecture 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 micro-architecture ''Golden Cove'' d'Intel dispose de cinq unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La micro-architecture ''Gracemont'' peut décoder 5 instructions, mais é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 quintuple é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 micro-architectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces micro-architectures utilisent le partage des ports d'émission au maximum. C'était des micro-architectures à 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 micro-architecture ''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 ''in-order'' à 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:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[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|Micro-architecture 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 micro-architecture 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.]] Le processeur alpha 21264 implémentait cette optimisation, mais c'était un processeur quadruple émission, aussi je le mentionne juste en passant. Il avait deux ALU entières et deux AGUs, les deux AGUs pouvaient faire des additions/soustractions et des opérations logiques. ===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ément 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. Les combinaisons autorisées sur l'alpha 21064 étaient : * une opération entière + une opération flottante ; * une opération flottante + un branchement flottant ; * une opération entière + un branchement entier ; * une instruction LOAD/STORE + toute autre opération, sauf exceptions vues plus bas. La seule limitation est donc que l'on ne peut pas exécuter deux branchements simultanés. Les possibilités interdites sont celles qui mêlent deux branchements entiers, deux branchements flottants, mais aussi un branchement entier + un branchement flottant. Pour le dernier cas, les exceptions sont les suivantes : * Une instruction STORE flottante + une opération entière ; * Une instruction STORE entière + une opération flottante. ===La double émission avec beaucoup d'unités entières=== 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 autre exemple récent est celui des processeurs ARM Cortex A53, A55 et A510. Le fait que ce soit des processeurs récents fait qu'ils peuvent implémenter une double émission assez poussée, car leur budget en transistors n'est pas aussi limité que leurs ancêtres. Les contraintes d’appariement sont assez faibles, ils peuvent émettre des paires d'instructions que les autres CPU en double émission ne peuvent pas émettre. Pour les opérations flottantes, ils ont deux ports d'émission flottantes, chacun alimentant un additionneur flottant et un multiplieur flottant. En conséquence, ils peuvent émettre deux multiplications flottantes ou deux additions flottantes en même temps, grâce à la présence de deux additionneurs flottants et de deux multiplieurs flottants. Par contre, les instructions dites SIMD ne supportent pas la double émission, mais laissons-les de côté. Pour les opérations entières, le processeur a deux ALU entières, deux ''barrel shifters'', un multiplieur entier (en réalité une unité MAD), un diviseur entier, une unité de branchement. Le processeur peut donc émettre deux opérations entières simples (additions/soustraction et décalages), mais une seule multiplication/division par cycle (en parallèle d'une opération ALU) et un seul branchement. Il y a aussi quelques contraintes d’appariement qui interdisent des paires d'instructions pourtant compatibles avec les unités présentes. Par exemple, on ne peut pas émettre un branchement avec une multiplication entière. Le pipeline mémoire est séparé des autres, afin d'implémenter les ''lectures non-bloquantes''. Le pipeline mémoire est composé d'une unité pour les µops LOAD et une autre pour les µops STORE. Malgré la présence de ces deux unités, le Cortex A53 ne peut émettre qu'une seule instruction par cycle. Son successeur, le A55, peut lui émettre une lecture et une écriture par cycle. Les deux unités LOAD et STORE ont leur propre unité d'adresse, les calculs d'adresse ne sont pas fait dans l'ALU entière. L'implémentation est donc plus simple, vu que l'absence d'exécution dans le désordre fait qu'on n'a pas besoin de faire le calcul d'adresse séparément, pas besoin de scinder les µops mémoire en deux. Le processeur incorpore aussi un réseau de contournement, pour l'unité mémoire. Les données lues dans le cache peuvent aller en entrée des ALU entières, mais aussi en entrée de l'unité LOAD. Cela accélère le cas où une lecture a une adresse lue par une lecture précédente. Les caches sont de type non-bloquant, avec cependant des contraintes assez fortes. ===La double émission de µops dépendantes=== De rares processeurs pouvaient émettre deux µops dépendantes simultanément. Leur idée était de fusionner deux µops consécutives en une seule µops, mais qui faisait plusieurs calculs consécutifs. L'unité de calcul entière était fortement modifiée, et devenait capable d’exécuter deux µops simples consécutives. Les exemples les plus connus sont le processeur SuperSPARC, le processeur POWER 1, et le Motorola 68060. Un exemple est celui du processeur POWER 1, qui pouvait émettre deux additions si elles avaient une dépendance. Pour cela, l'ALU entière intègre un additionneur trois-opérande, ce qui permet de faire le calcul A + B + C. Le processeur SuperSPARC allait plus loin. Il disposait de trois ALU entières enchainées. Les deux premières ALU pouvaient travailler indépendamment les unes des autres. Le processeur peut émettre deux µops entières dedans, les deux ALUs vont en traiter une chacune. Mais les deux µops sont dépendantes, les deux ALU envoient leurs résultat àç une troisième ALU, qui combine les deux résultats. <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> jmc2e2632dzyb1bzc80vgbaayiinlq4 775116 775079 2026-10-12T23:48:13Z Mewtow 31375 /* La double émission de µops dépendantes */ 775116 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 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 très grosse majorité des CPUs double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une 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 || |- ! ... || ... || ... || ... || ... || ... || ... |} De nos jours, même les processeurs superscalaires utilisent l'exécution dans le désordre, et sont tous superscalaires. Et ce même pour les processeurs basse consommation. La loi de Moore nous permet d'utiliser un budget en transistor permettant une exécution dans le désordre minimale, qui améliore grandement les performances pour une consommation en énergie très faible. Les processeurs superscalaires utilisent donc tous l’exécution dans le désordre. Tous ? Non ! Une poignée d'irréductibles processeurs ARM résistent encore et toujours à la tendance globale. ARM a une gamme de processeurs superscalaires sans exécution dans le désordre, qui sont séparés de leurs autres processeurs superscalaires plus puissants. Il s'agit des processeurs ARM Cortex A53, A55, et A510. Il s'agit de processeurs double ou triple émission, prévu pour des applications embarquées, qui privilégient la très basse consommation sur les performances. Ils étaient utilisés dans des ''System On Chip'' assez divers, où ils étaient soit utilisés seuls, soit couplés à des cœurs plus puissants et avec une microarchitecture plus évoluée. ==Le ''front-end'' des processeurs superscalaires ''in-order''== 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 depuis le cache d'instruction (ou depuis la mémoire RAM). Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc 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, les instructions d'un bloc sont chargées, décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après un branchement pris 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 blocs d'instructions doivent être alignées sur 128 bits. La moindre instruction à cheval sur deux blocs posera une erreur d'alignement, que le CPU devrait gérer. Et on peut généraliser à n'importe quelle taille de bloc. Il arrivera qu'une instruction est à 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 superscalaires utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est 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 ''in-order''== 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’appariement 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. 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 pour un multiplieur et un ''barrel shifter'' 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 |} Précisons cependant que quelques processeurs offrent un port dédié pour le multiplieur entier. Les processeurs ARM haute performance sont souvent dans ce cas, avec un port d'émission dédié au multiplieur entier. Mais ils sont l'exception qui confirme la règle. 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 dupliqué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 micro-architecture 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 micro-architecture ''Golden Cove'' d'Intel dispose de cinq unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La micro-architecture ''Gracemont'' peut décoder 5 instructions, mais é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 quintuple é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 micro-architectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces micro-architectures utilisent le partage des ports d'émission au maximum. C'était des micro-architectures à 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 micro-architecture ''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 ''in-order'' à 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:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[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|Micro-architecture 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 micro-architecture 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.]] Le processeur alpha 21264 implémentait cette optimisation, mais c'était un processeur quadruple émission, aussi je le mentionne juste en passant. Il avait deux ALU entières et deux AGUs, les deux AGUs pouvaient faire des additions/soustractions et des opérations logiques. ===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ément 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. Les combinaisons autorisées sur l'alpha 21064 étaient : * une opération entière + une opération flottante ; * une opération flottante + un branchement flottant ; * une opération entière + un branchement entier ; * une instruction LOAD/STORE + toute autre opération, sauf exceptions vues plus bas. La seule limitation est donc que l'on ne peut pas exécuter deux branchements simultanés. Les possibilités interdites sont celles qui mêlent deux branchements entiers, deux branchements flottants, mais aussi un branchement entier + un branchement flottant. Pour le dernier cas, les exceptions sont les suivantes : * Une instruction STORE flottante + une opération entière ; * Une instruction STORE entière + une opération flottante. ===La double émission avec beaucoup d'unités entières=== 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 autre exemple récent est celui des processeurs ARM Cortex A53, A55 et A510. Le fait que ce soit des processeurs récents fait qu'ils peuvent implémenter une double émission assez poussée, car leur budget en transistors n'est pas aussi limité que leurs ancêtres. Les contraintes d’appariement sont assez faibles, ils peuvent émettre des paires d'instructions que les autres CPU en double émission ne peuvent pas émettre. Pour les opérations flottantes, ils ont deux ports d'émission flottantes, chacun alimentant un additionneur flottant et un multiplieur flottant. En conséquence, ils peuvent émettre deux multiplications flottantes ou deux additions flottantes en même temps, grâce à la présence de deux additionneurs flottants et de deux multiplieurs flottants. Par contre, les instructions dites SIMD ne supportent pas la double émission, mais laissons-les de côté. Pour les opérations entières, le processeur a deux ALU entières, deux ''barrel shifters'', un multiplieur entier (en réalité une unité MAD), un diviseur entier, une unité de branchement. Le processeur peut donc émettre deux opérations entières simples (additions/soustraction et décalages), mais une seule multiplication/division par cycle (en parallèle d'une opération ALU) et un seul branchement. Il y a aussi quelques contraintes d’appariement qui interdisent des paires d'instructions pourtant compatibles avec les unités présentes. Par exemple, on ne peut pas émettre un branchement avec une multiplication entière. Le pipeline mémoire est séparé des autres, afin d'implémenter les ''lectures non-bloquantes''. Le pipeline mémoire est composé d'une unité pour les µops LOAD et une autre pour les µops STORE. Malgré la présence de ces deux unités, le Cortex A53 ne peut émettre qu'une seule instruction par cycle. Son successeur, le A55, peut lui émettre une lecture et une écriture par cycle. Les deux unités LOAD et STORE ont leur propre unité d'adresse, les calculs d'adresse ne sont pas fait dans l'ALU entière. L'implémentation est donc plus simple, vu que l'absence d'exécution dans le désordre fait qu'on n'a pas besoin de faire le calcul d'adresse séparément, pas besoin de scinder les µops mémoire en deux. Le processeur incorpore aussi un réseau de contournement, pour l'unité mémoire. Les données lues dans le cache peuvent aller en entrée des ALU entières, mais aussi en entrée de l'unité LOAD. Cela accélère le cas où une lecture a une adresse lue par une lecture précédente. Les caches sont de type non-bloquant, avec cependant des contraintes assez fortes. ===La double émission de µops dépendantes=== De rares processeurs pouvaient émettre deux µops dépendantes simultanément. Leur idée était de fusionner deux µops consécutives en une seule µops, mais qui faisait plusieurs calculs consécutifs. L'unité de calcul entière était fortement modifiée, et devenait capable d’exécuter deux µops simples consécutives. Les exemples les plus connus sont le processeur SuperSPARC, le processeur POWER 1, et le Motorola 68060. Un exemple est celui du processeur POWER 1, qui pouvait émettre deux additions si elles avaient une dépendance. Pour cela, l'ALU entière intègre un additionneur trois-opérande, ce qui permet de faire le calcul A + B + C. Le processeur SuperSPARC allait plus loin. Il disposait de trois ALU entières enchainées. Les deux premières ALU pouvaient travailler indépendamment les unes des autres. Le processeur peut émettre deux µops entières dedans, les deux ALUs vont en traiter une chacune. Mais les deux µops sont dépendantes, les deux ALU envoient leurs résultat à une troisième ALU, qui combine les deux résultats. <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> 4mc1dmtm1hpsrvsoc5jn6x44c3hjfsd 775117 775116 2026-10-13T00:04:06Z Mewtow 31375 /* La double émission de µops dépendantes */ 775117 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 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 très grosse majorité des CPUs double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une 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 || |- ! ... || ... || ... || ... || ... || ... || ... |} De nos jours, même les processeurs superscalaires utilisent l'exécution dans le désordre, et sont tous superscalaires. Et ce même pour les processeurs basse consommation. La loi de Moore nous permet d'utiliser un budget en transistor permettant une exécution dans le désordre minimale, qui améliore grandement les performances pour une consommation en énergie très faible. Les processeurs superscalaires utilisent donc tous l’exécution dans le désordre. Tous ? Non ! Une poignée d'irréductibles processeurs ARM résistent encore et toujours à la tendance globale. ARM a une gamme de processeurs superscalaires sans exécution dans le désordre, qui sont séparés de leurs autres processeurs superscalaires plus puissants. Il s'agit des processeurs ARM Cortex A53, A55, et A510. Il s'agit de processeurs double ou triple émission, prévu pour des applications embarquées, qui privilégient la très basse consommation sur les performances. Ils étaient utilisés dans des ''System On Chip'' assez divers, où ils étaient soit utilisés seuls, soit couplés à des cœurs plus puissants et avec une microarchitecture plus évoluée. ==Le ''front-end'' des processeurs superscalaires ''in-order''== 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 depuis le cache d'instruction (ou depuis la mémoire RAM). Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc 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, les instructions d'un bloc sont chargées, décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après un branchement pris 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 blocs d'instructions doivent être alignées sur 128 bits. La moindre instruction à cheval sur deux blocs posera une erreur d'alignement, que le CPU devrait gérer. Et on peut généraliser à n'importe quelle taille de bloc. Il arrivera qu'une instruction est à 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 superscalaires utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est 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 ''in-order''== 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’appariement 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. 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 pour un multiplieur et un ''barrel shifter'' 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 |} Précisons cependant que quelques processeurs offrent un port dédié pour le multiplieur entier. Les processeurs ARM haute performance sont souvent dans ce cas, avec un port d'émission dédié au multiplieur entier. Mais ils sont l'exception qui confirme la règle. 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 dupliqué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 micro-architecture 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 micro-architecture ''Golden Cove'' d'Intel dispose de cinq unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La micro-architecture ''Gracemont'' peut décoder 5 instructions, mais é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 quintuple é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 micro-architectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces micro-architectures utilisent le partage des ports d'émission au maximum. C'était des micro-architectures à 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 micro-architecture ''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 ''in-order'' à 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:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[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|Micro-architecture 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 micro-architecture 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.]] Le processeur alpha 21264 implémentait cette optimisation, mais c'était un processeur quadruple émission, aussi je le mentionne juste en passant. Il avait deux ALU entières et deux AGUs, les deux AGUs pouvaient faire des additions/soustractions et des opérations logiques. ===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ément 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. Les combinaisons autorisées sur l'alpha 21064 étaient : * une opération entière + une opération flottante ; * une opération flottante + un branchement flottant ; * une opération entière + un branchement entier ; * une instruction LOAD/STORE + toute autre opération, sauf exceptions vues plus bas. La seule limitation est donc que l'on ne peut pas exécuter deux branchements simultanés. Les possibilités interdites sont celles qui mêlent deux branchements entiers, deux branchements flottants, mais aussi un branchement entier + un branchement flottant. Pour le dernier cas, les exceptions sont les suivantes : * Une instruction STORE flottante + une opération entière ; * Une instruction STORE entière + une opération flottante. ===La double émission avec beaucoup d'unités entières=== 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 autre exemple récent est celui des processeurs ARM Cortex A53, A55 et A510. Le fait que ce soit des processeurs récents fait qu'ils peuvent implémenter une double émission assez poussée, car leur budget en transistors n'est pas aussi limité que leurs ancêtres. Les contraintes d’appariement sont assez faibles, ils peuvent émettre des paires d'instructions que les autres CPU en double émission ne peuvent pas émettre. Pour les opérations flottantes, ils ont deux ports d'émission flottantes, chacun alimentant un additionneur flottant et un multiplieur flottant. En conséquence, ils peuvent émettre deux multiplications flottantes ou deux additions flottantes en même temps, grâce à la présence de deux additionneurs flottants et de deux multiplieurs flottants. Par contre, les instructions dites SIMD ne supportent pas la double émission, mais laissons-les de côté. Pour les opérations entières, le processeur a deux ALU entières, deux ''barrel shifters'', un multiplieur entier (en réalité une unité MAD), un diviseur entier, une unité de branchement. Le processeur peut donc émettre deux opérations entières simples (additions/soustraction et décalages), mais une seule multiplication/division par cycle (en parallèle d'une opération ALU) et un seul branchement. Il y a aussi quelques contraintes d’appariement qui interdisent des paires d'instructions pourtant compatibles avec les unités présentes. Par exemple, on ne peut pas émettre un branchement avec une multiplication entière. Le pipeline mémoire est séparé des autres, afin d'implémenter les ''lectures non-bloquantes''. Le pipeline mémoire est composé d'une unité pour les µops LOAD et une autre pour les µops STORE. Malgré la présence de ces deux unités, le Cortex A53 ne peut émettre qu'une seule instruction par cycle. Son successeur, le A55, peut lui émettre une lecture et une écriture par cycle. Les deux unités LOAD et STORE ont leur propre unité d'adresse, les calculs d'adresse ne sont pas fait dans l'ALU entière. L'implémentation est donc plus simple, vu que l'absence d'exécution dans le désordre fait qu'on n'a pas besoin de faire le calcul d'adresse séparément, pas besoin de scinder les µops mémoire en deux. Le processeur incorpore aussi un réseau de contournement, pour l'unité mémoire. Les données lues dans le cache peuvent aller en entrée des ALU entières, mais aussi en entrée de l'unité LOAD. Cela accélère le cas où une lecture a une adresse lue par une lecture précédente. Les caches sont de type non-bloquant, avec cependant des contraintes assez fortes. ===La double émission de µops dépendantes=== De rares processeurs pouvaient émettre deux µops dépendantes simultanément. Leur idée était de fusionner deux µops consécutives en une seule µops, mais qui faisait plusieurs calculs consécutifs. L'unité de calcul entière était fortement modifiée, et devenait capable d’exécuter deux µops simples consécutives. Les exemples les plus connus sont le processeur SuperSPARC, le processeur POWER 1, et le Motorola 68060. Un exemple est celui du processeur POWER 1, qui pouvait émettre deux additions si elles avaient une dépendance. Pour cela, l'ALU entière intègre un additionneur trois-opérande, ce qui permet de faire le calcul A + B + C. Le processeur SuperSPARC allait plus loin. Il disposait de trois ALU entières enchainées. Les deux premières ALU pouvaient travailler indépendamment les unes des autres. Le processeur peut émettre deux µops entières dedans, les deux ALUs vont en traiter une chacune. Mais les deux µops sont dépendantes, les deux ALU envoient leurs résultat à une troisième ALU, qui combine les deux résultats. De telles techniques ne fonctionnent qu'avec la double émission, pour des paires d'instructions bien précises. Par exemple, il est impossible d'exécuter des paires de lectures/écriture dépendantes, l'une utilisant le résultat de l'autre. Seules les paires d'instructions arithmétiques bien précises peuvent être émises ainsi. Si on voulait adapter ces techniques à la triple ou quadruple émission, le cout en ALU entières serait trop important. <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> iev4latbyrlzrm3flgtkbd16hm7rrqh 775118 775117 2026-10-13T00:05:21Z Mewtow 31375 /* La double émission de µops dépendantes */ 775118 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 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 très grosse majorité des CPUs double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une 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 || |- ! ... || ... || ... || ... || ... || ... || ... |} De nos jours, même les processeurs superscalaires utilisent l'exécution dans le désordre, et sont tous superscalaires. Et ce même pour les processeurs basse consommation. La loi de Moore nous permet d'utiliser un budget en transistor permettant une exécution dans le désordre minimale, qui améliore grandement les performances pour une consommation en énergie très faible. Les processeurs superscalaires utilisent donc tous l’exécution dans le désordre. Tous ? Non ! Une poignée d'irréductibles processeurs ARM résistent encore et toujours à la tendance globale. ARM a une gamme de processeurs superscalaires sans exécution dans le désordre, qui sont séparés de leurs autres processeurs superscalaires plus puissants. Il s'agit des processeurs ARM Cortex A53, A55, et A510. Il s'agit de processeurs double ou triple émission, prévu pour des applications embarquées, qui privilégient la très basse consommation sur les performances. Ils étaient utilisés dans des ''System On Chip'' assez divers, où ils étaient soit utilisés seuls, soit couplés à des cœurs plus puissants et avec une microarchitecture plus évoluée. ==Le ''front-end'' des processeurs superscalaires ''in-order''== 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 depuis le cache d'instruction (ou depuis la mémoire RAM). Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc 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, les instructions d'un bloc sont chargées, décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après un branchement pris 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 blocs d'instructions doivent être alignées sur 128 bits. La moindre instruction à cheval sur deux blocs posera une erreur d'alignement, que le CPU devrait gérer. Et on peut généraliser à n'importe quelle taille de bloc. Il arrivera qu'une instruction est à 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 superscalaires utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est 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 ''in-order''== 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’appariement 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. 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 pour un multiplieur et un ''barrel shifter'' 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 |} Précisons cependant que quelques processeurs offrent un port dédié pour le multiplieur entier. Les processeurs ARM haute performance sont souvent dans ce cas, avec un port d'émission dédié au multiplieur entier. Mais ils sont l'exception qui confirme la règle. 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 dupliqué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 micro-architecture 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 micro-architecture ''Golden Cove'' d'Intel dispose de cinq unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La micro-architecture ''Gracemont'' peut décoder 5 instructions, mais é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 quintuple é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 micro-architectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces micro-architectures utilisent le partage des ports d'émission au maximum. C'était des micro-architectures à 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 micro-architecture ''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 ''in-order'' à 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:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] |[[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|Micro-architecture 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 micro-architecture 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.]] Le processeur alpha 21264 implémentait cette optimisation, mais c'était un processeur quadruple émission, aussi je le mentionne juste en passant. Il avait deux ALU entières et deux AGUs, les deux AGUs pouvaient faire des additions/soustractions et des opérations logiques. ===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ément 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. Les combinaisons autorisées sur l'alpha 21064 étaient : * une opération entière + une opération flottante ; * une opération flottante + un branchement flottant ; * une opération entière + un branchement entier ; * une instruction LOAD/STORE + toute autre opération, sauf exceptions vues plus bas. La seule limitation est donc que l'on ne peut pas exécuter deux branchements simultanés. Les possibilités interdites sont celles qui mêlent deux branchements entiers, deux branchements flottants, mais aussi un branchement entier + un branchement flottant. Pour le dernier cas, les exceptions sont les suivantes : * Une instruction STORE flottante + une opération entière ; * Une instruction STORE entière + une opération flottante. ===La double émission avec beaucoup d'unités entières=== 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 autre exemple récent est celui des processeurs ARM Cortex A53, A55 et A510. Le fait que ce soit des processeurs récents fait qu'ils peuvent implémenter une double émission assez poussée, car leur budget en transistors n'est pas aussi limité que leurs ancêtres. Les contraintes d’appariement sont assez faibles, ils peuvent émettre des paires d'instructions que les autres CPU en double émission ne peuvent pas émettre. Pour les opérations flottantes, ils ont deux ports d'émission flottantes, chacun alimentant un additionneur flottant et un multiplieur flottant. En conséquence, ils peuvent émettre deux multiplications flottantes ou deux additions flottantes en même temps, grâce à la présence de deux additionneurs flottants et de deux multiplieurs flottants. Par contre, les instructions dites SIMD ne supportent pas la double émission, mais laissons-les de côté. Pour les opérations entières, le processeur a deux ALU entières, deux ''barrel shifters'', un multiplieur entier (en réalité une unité MAD), un diviseur entier, une unité de branchement. Le processeur peut donc émettre deux opérations entières simples (additions/soustraction et décalages), mais une seule multiplication/division par cycle (en parallèle d'une opération ALU) et un seul branchement. Il y a aussi quelques contraintes d’appariement qui interdisent des paires d'instructions pourtant compatibles avec les unités présentes. Par exemple, on ne peut pas émettre un branchement avec une multiplication entière. Le pipeline mémoire est séparé des autres, afin d'implémenter les ''lectures non-bloquantes''. Le pipeline mémoire est composé d'une unité pour les µops LOAD et une autre pour les µops STORE. Malgré la présence de ces deux unités, le Cortex A53 ne peut émettre qu'une seule instruction par cycle. Son successeur, le A55, peut lui émettre une lecture et une écriture par cycle. Les deux unités LOAD et STORE ont leur propre unité d'adresse, les calculs d'adresse ne sont pas fait dans l'ALU entière. L'implémentation est donc plus simple, vu que l'absence d'exécution dans le désordre fait qu'on n'a pas besoin de faire le calcul d'adresse séparément, pas besoin de scinder les µops mémoire en deux. Le processeur incorpore aussi un réseau de contournement, pour l'unité mémoire. Les données lues dans le cache peuvent aller en entrée des ALU entières, mais aussi en entrée de l'unité LOAD. Cela accélère le cas où une lecture a une adresse lue par une lecture précédente. Les caches sont de type non-bloquant, avec cependant des contraintes assez fortes. ===La double émission de µops dépendantes=== De rares processeurs pouvaient émettre deux µops dépendantes simultanément. Leur idée était de fusionner deux µops consécutives en une seule µops, mais qui faisait plusieurs calculs consécutifs. L'unité de calcul entière était fortement modifiée, et devenait capable d’exécuter deux µops simples consécutives. Les exemples les plus connus sont le processeur SuperSPARC, le processeur POWER 1, et le Motorola 68060. Un exemple est celui du processeur POWER 1, qui pouvait émettre deux additions si elles avaient une dépendance. Pour cela, l'ALU entière intègre un additionneur trois-opérande, ce qui permet de faire le calcul A + B + C. Le processeur SuperSPARC allait plus loin. Il disposait de trois ALU entières enchainées. Les deux premières ALU pouvaient travailler indépendamment les unes des autres. Le processeur peut émettre deux µops entières dedans, les deux ALUs vont en traiter une chacune. Mais les deux µops sont dépendantes, les deux ALU envoient leurs résultat à une troisième ALU, qui combine les deux résultats. De telles techniques ne fonctionnent qu'avec la double émission, pour des paires d'instructions bien précises. Par exemple, il est impossible d'exécuter des paires de lectures/écriture dépendantes, l'une utilisant le résultat de l'autre. idem avec les paires d'instructions flottantes, vu que dupliquer la FPU aurait un cout en circuits trop important. Seules les paires d'instructions arithmétiques entières peuvent être émises ainsi, et encore : sans multiplications ou autres opérations complexes. Si on voulait adapter ces techniques à la triple ou quadruple émission, le cout en ALU entières serait trop important. <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> jktssd49onxdpgxesu5tyafk0h7agfo Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire 0 68397 775123 763905 2026-10-13T02:22:26Z Mewtow 31375 /* L'exemple avec le x86 */ 775123 wikitext text/x-wiki Afin de gérer le partage de la mémoire sans problèmes, chaque processeur doit définir un '''modèle mémoire''', un ensemble de restrictions et de contraintes quant à l'interaction avec la mémoire RAM. La première contrainte garantit que les instructions ne puissent pas être interrompues (ou donnent l'impression de ne pas l'être) : c'est la propriété d''''atomicité'''. Un thread doit utiliser plusieurs instructions successives sur la donnée pour pouvoir en faire ce qu'il veut, et cela peut poser des problèmes si les instructions peuvent être interrompues par une exception ou tout autre chose. Par exemple, il est possible qu'une lecture démarre avant que la précédente soit terminée. De même, rien n’empêche une lecture de finir avant l'écriture précédente et renvoyer la valeur d'avant l'écriture. Prenons un exemple, avec un entier utilisé par plusieurs threads, chaque thread s’exécutant sur un processeur x86. Chaque thread veut l'incrémenter. Seul problème, l'incrémentation n'est pas effectuée en une seule instruction sur les processeurs x86. Il faut en effet lire la donnée, l'augmenter de 1, puis l'écrire. Ce qui fait que l'on peut se retrouver dans la situation illustrée ci-dessous, où un processeur n'a pas eu le temps de finir son incrémentation qu'un autre en a démarré une nouvelle. [[File:Illustration du résultat de deux opérations concurrentes sur la même variable.png|centre|vignette|upright=2|Illustration du résultat de deux opérations concurrentes sur la même variable.]] Pour avoir le bon résultat il y a une seule et unique solution : le processeur qui accède à la donnée doit avoir un accès exclusif à la donnée partagée. Sans cela, l'autre processeur ira lire une version de la donnée pas encore modifiée par le premier processeur. Dans notre exemple, un seul thread doit pouvoir manipuler notre compteur à la fois. Et bien sûr, cette réponse se généralise à presque toutes les autres situations impliquant une donnée partagée. On doit donc définir ce qu'on appelle une '''section critique''' : un morceau de temps durant lequel un thread aura un accès exclusif à une donnée partagée, avec la certitude qu'aucun autre thread ne peut modifier la donnée partagée durant ce temps. Autant prévenir tout de suite, créer de telles sections critiques se base sur des mécanismes mêlant le matériel et le logiciel. Il existe deux grandes solutions, qui peuvent être soit codées sous la forme de programmes, soit implantées directement dans le silicium de nos processeurs. L''''exclusion mutuelle''' permet à un thread de réserver la donnée partagée. Un thread qui veut manipuler une donnée réserve celle-ci, et la libère une fois qu'il en a fini avec elle. Si la donnée est réservée, tous les autres threads attendent leur tour. Pour mettre en œuvre cette réservation/dé-réservation, on ajoute un compteur à la donnée partagée, qui indique si la donnée partagée est libre ou déjà réservée. Dans le cas le plus simple, ce compteur vaudra 0 si la donnée est réservée, et 1 si elle est libre. Ce compteur ce qu'on appelle un verrou d'exclusion mutuelle, aussi appelé ''mutex'' (raccourci du terme anglais '''''mut'''ual '''ex'''clusion''). [[File:Mutex.png|centre|vignette|upright=2.5|Mutex.]] ==Les instructions atomiques== Dans le cas précédent, la vérification et modification du compteur ne peut pas être interrompue, sous peine de problèmes. On peut reprendre l'exemple du dessus pour l'illustrer. Si notre compteur est à 0, et que deux threads veulent lire et modifier ce compteur simultanément, il est possible que les deux threads lisent le compteur en même temps : ils liront alors un zéro, et essayeront alors de se réserver la donnée simultanément. Bref, retour à la case départ... ===Les instructions atomiques, niveau jeu d'instruction=== Idéalement, il faudrait que lecture et écriture se fassent en une seule fois. Pour régler ce problème, certains processeurs fournissent des instructions spécialisées, in-interruptibles, capables d'effectuer cette modification du compteur en une seule fois. Elles peuvent lire le compteur, décider si on peut le modifier, et écrire la bonne valeur sans être dérangé par un autre processeur qui viendrait s'inviter dans la mémoire sans autorisation ! Par exemple, sur les processeurs x86, la vérification/modification du compteur vue plus haut peut se faire avec l'instruction ''test and set''. Des instructions similaires existent pour résoudre ce genre de problèmes. Leur rôle est toujours d'implémenter des verrous d'exclusion mutuelle plus ou moins sophistiqués, comme des sémaphores, des verrous (''Locks''), etc. Elles sont appelées des '''instructions atomiques'''. De telles instructions empêchent tout accès mémoire tant qu'elles ne sont pas terminées. Généralement, un programmeur n'a pas à manipuler des instructions atomiques lui-même, mais manipule des abstractions basées sur ces instructions atomiques, fournies par des bibliothèques ou son langage de programmation. Voici la plupart de ces instructions atomiques les plus connues : {|class="wikitable" |- ! Instruction !! Description |- ! Compare And Swap | Cette instruction prend deux opérandes : une utilisée pour une comparaison, une autre à écrire en mémoire RAM. Elle lit une adresse, compare la donnée lue au premier opérande, et copie le second opérande à la même adresse si la comparaison échoue. La comparaison vérifie si la donnée lue et l'opérande sont égales, l'écriture a lieu si elles ne le sont pas. |- ! Fetch And Add | ''Fetch And Add'' incrémente un compteur en mémoire RAM en une seule fois. Elle effectue une lecture du compteur, son incrémentation et l'écriture du résultat en une seule instruction. Elle permet de réaliser ce qu'on appelle des sémaphores ou des compteurs concurrents. |- ! XCHG | Elle échange le contenu d'un registre et d'une adresse de façon atomique. Elle est notoirement utilisée sur les processeurs x86 de nos PC. |} ===Les instructions atomiques, niveau matériel=== L'implémentation la plus simple demande que les processeurs partagent un bus mémoire et aient des caches dédiés. Lors de l’exécution de l'instruction atomique, aucun processeur ne peut aller manipuler la mémoire. L'instruction atomique réserve l'accès au bus et seul le processeur peut accèder à la mémoire tant que l'instruction atomique n'est pas terminée. La réservation se fait en configurant un bit du bus mémoire, appelé le bit LOCK. D'où le terme de '''''bus lock''''' utilisé pour cette solution. Le cout de ce blocage de la mémoire est assez lourd, ce qui rend les instructions atomiques assez lentes. Une solution alternative bloque une ligne de cache, afin qu'aucun autre cœur ne puisse lire ou écrire la ligne de cache réservée. La réservation utilise le protocole de cohérence des caches. Un CPU réserve une ligne de cache en la mettant en état ''Exclusive'' dans son cache. Il suffit alors de maintenir cet état durant la durée de l'instruction pour avoir une instruction atomique. Dans ce cas, pas besoin de bloquer la mémoire, les autres processeurs peuvent faire des lectures et écritures sans problème, tant qu'elles se font dans d'autres lignes de cache. Le coût des instructions atomiques est alors fortement amorti. Il faut noter que la donnée lue/écrite peut se situer à cheval entre deux lignes de cache. C'est ce qu'on appelle un '''''split lock'''''. La première demandent de réserver deux lignes de cache, mais il faut éviter un tas de problèmes tordus du style : un coeur réserve la première ligne de cache, un autre réserve la seconde. Une autre solution bascule sur un ''bus lock'' pour ce genre d'accès non-aligné, mais elle ne marche que sur les processeurs avec un bus partagé. Leur implémentation sur les processeurs x86 modernes est assez mal connue, vu qu'ils n'utilisent pas de bus partagé. * Pour plus d'informations, notamment des mesures expérimentales, allez lire cet article : [https://chipsandcheese.com/p/investigating-split-locks-on-x86 Investigating Split Locks on x86-64]. Sur un processeur avec désambiguïsation mémoire de type x86, il faut attendre que la file d'écriture soit vidée avant de démarrer une instruction atomique. La raison est que les instructions atomiques font une lecture, une opération, et une écriture, dans cet ordre. En théorie, la lecture peut passer avant une écriture précédente, à une adresse différente. Mais dans ce cas, cela signifierait que l'écriture aussi serait déplacée avant, car la lecture est liée à l'écriture les deux se font de manière atomique. Et faire passer une écriture avant une autre n'est pas compatible avec les règles du ''total store ordering''. ==Les instructions LL/SC== Une autre technique de synchronisation est basée sur les instructions '''Load-Link''' et '''Store-Conditional'''. L'instruction Load-Link lit une donnée depuis la mémoire de façon atomique. L'instruction Store-Conditional écrit une donnée chargée avec Load-Link, mais uniquement à condition que la copie en mémoire n'aie pas été modifiée entre temps. Si ce n'est pas le cas, Store-conditional échoue. Pour indiquer un échec, il y a plusieurs solutions : soit elle met un bit du registre d'état à 1, soit elle écrit une valeur de retour dans un registre. Sur certains processeurs, l’exécution d'interruptions ou d'exceptions matérielles après un Load-Link fait échouer un Store-conditional ultérieur. Implémenter ces deux instructions est assez simple, et peut se faire en utilisant les techniques de ''bus-snopping'' vues dans le chapitre sur la cohérence des caches. Pour implémenter l'instruction SC, il suffit de mémoriser si la donnée lue par l'instruction LL a été invalidée depuis la dernière instruction LL. Pour cela, on utilise un registre qui mémorise l'adresse lue par l'instruction LL, à laquelle on ajoute un bit d'invalidation qui dit si cette adresse a été invalidée. L'instruction LL va initialiser le registre d'adresse avec l'adresse lue, et le bit est mis à zéro. Une écriture a lieu sur le bus, des circuits vérifient si l'adresse écrite est identique à celle contenue dans le registre d'adresse et mettent à jour le bit d'invalidation. L'instruction SC doit vérifier ce bit avant d'autoriser l'écriture. ==La mémoire Transactionnelle Matérielle== La '''mémoire transactionnelle''' permet de rendre atomiques des morceaux de programmes complets. Les morceaux de programmes rendus atomiques sont appelés des ''transactions''. Pendant qu'une transaction s'exécute, tout se passe comme si la transaction ne modifiait pas de données et restait plus ou moins "invisible" des autres processeurs. Une fois terminée, le processeur vérifie s'il y a eu un conflit d'accès avec les autres processeurs. Si c'est le cas, la transaction échoue et doit reprendre depuis le début : les changements effectués par la transaction ne seront pas pris en compte. Mais s'il n'y a pas eu conflit d'accès, alors la transaction a réussi et elle peut écrire son résultat en mémoire. Définir une transaction demande d'ajouter plusieurs instructions : une pour démarrer une transaction, une autre pour la terminer, éventuellement une troisième pour annuler précocement une transaction. Lorsqu'une transaction échoue, elle laisse la main au logiciel. Précisément, elle fait un branchement vers une fonction qui gère l'échec de la transaction. La fonction décide s'il faut ré-exécuter la transaction, attendre un peu avant de la re-démarrer, ou faire autre chose. Un autre point est que quand une transaction échoue, les registres doivent être remis à leur valeur initiale, celle d'avant la transaction. Et cette restauration est déléguée au code de gestion d'échec de transaction. Il est aussi possible de gérer la sauvegarde des registres en matériel, avec un système de ''checkpoints'', déjà abordé dans le chapitre sur les processeurs à émission dans l'ordre, dans la section sur les interruptions et le pipeline. Reste à détecter les conflits d'accès. ===La mémoire transactionnelle explicite : la réservation des lignes de cache=== La mémoire transactionnelle se décline en deux versions principales, que nous allons qualifier d'explicite et d'implicite. La mémoire transactionnelle implicite est la plus simple conceptuellement : les lectures et écritures utilisées dans la transaction sont des instructions d'accès mémoire normales, mais sont gérées de manière transactionnelle lors de la transaction. Une autre solution utilise des '''accès mémoires transactionnels explicites''', qui ajoute des instructions d'accès mémoire spécialisées pour les transactions. Il est possible d'exécuter des instructions mémoire normale dans une transaction, qui s'exécutent même si la transaction échoue. Les instructions LOAD/STORE transactionnelles sont les seules à être annulées si la transaction échoue. L'instruction de lecture transactionnelle réserve une ligne de cache pour le processeur. Par réserver, on veut dire que le processeur est le seul à avoir accès en écriture à cette ligne de cache. Si un autre processeur tente d'écrire dans cette ligne de cache, la transaction fautive est annulée. Il est aussi possible de libérer une ligne de cache réservée avec une instruction RELEASE, complémentaire des instructions de lecture/réservation. Pour résumer, il y a donc trois instructions mémoire transactionnelles : READ/RESERVE, WRITE-IF-RESERVED, et RELEASE. Vous aurez peut-être fait le lien avec les instructions LINK-LOAD et STORE-CONDITIONNAL, et il faut avouer que les instructions mémoire transactionnelles ressemblent un petit peu. Disons que ce sont des variantes adaptées au fait qu'elles s'exécutent dans des transactions. De plus, LINK-LOAD et STORE-CONDITIONNAL ne réservent pas des lignes de cache, elles se contentent de vérifier qu'une écriture n'a pas eu lieu entre la lecture et l'écriture. Pour gérer les réservations, le processeur incorpore un '''registre de réservation''' par ligne de cache réservée. Le registre mémorise la donnée lue ou écrite. Les écritures se font dans ce registre, elles ne sont pas propagées dans le cache. Par contre, il est possible que ces registres soient pris en compte par les mécanismes de cohérence des caches, afin de gérer la détection des conflits. Les transactions explicites sont plus flexibles, mais posent plus de problèmes d'implémentation que les transactions implicites. La réservation des lignes de cache est compliquée à implémenter. En comparaison, les méthodes implicites sont plus simples à comprendre et à implémenter. Nous allons nous concentrer sur les méthodes implicites dans le reste du chapitre. ===La mémoire transactionnelle implicite : les ensembles de lecture et d'écriture=== Toute transaction lit un ensemble de données appelé l'<nowiki/>'''ensemble de lecture'',''''' et écrit/modifie des données qui forment l'<nowiki/>'''ensemble d'écriture'''. Le processeur doit identifier l'ensemble de lecture et d'écriture quelque part pour détecter les conflits. Deux conditions font qu'une transaction peut échouer. La première est quand un autre processeur a écrit une donnée dans l'ensemble de lecture. La seconde est quand un autre processeur a lu ou écrit une donnée dans l'ensemble d'écriture. Par contre, il n'y a pas de problème si une donnée est lue par un autre processeur dans l'ensemble de lecture. Mémoriser l'ensemble de lecture est assez simple : il suffit d'ajouter un bit READ à chaque ligne de cache, qui indique qu'elle a été lue par une transaction. Par contre, la gestion de l'ensemble d'écriture est plus complexe. Il y a deux méthodes : une qui autorise les écritures dans le cache, une autre qui met en attente les écritures. ====L'implémentation avec un ensemble d'écritures dans la ''Store Queue''==== Une première méthode met en attente les écritures, qui ne sont propagées dans le cache qu'une fois que la transaction est terminée. Elle mémorise l'ensemble de lecture dans le cache, mais l'ensemble d'écriture reste dans la ''Store Queue'', la ''Load-Store Queue'' ou autre. La technique en question a été implémentée pour la première fois sur les processeurs Transmetta Crusoe et Efficeon, sous le nom de '''''gated store buffer'''''. Avec cette technique, les données sont bel et bien lues depuis le cache, mais les écritures ne sont pas propagées dans le cache, elles restent dans le pipeline. À la fin de la transaction, le processeur vérifie s'il n'y a pas eu de conflits et si la transaction est validée. Si ce n'est pas le cas, la ''Store Queue''/''Load-Store Queue'' est vidée, ce qui annule les changements effectués par la transaction (ROB ou autre méthode capable de gérer les interruptions précises). Si c'est le cas, les écritures sont propagées dans le cache. Si la transaction réussit, le processeur doit vider la ''Store Queue'' dedans. Les écritures se font une par une, ce qui peut prendre un peu de temps, surtout si la transaction a fait beaucoup d'écritures. Et pendant le temps, les données écrites peuvent en théorie être écrasées par une écriture provenant d'un autre cœur. Pour éviter cela, le processeur peut interdire les écritures dans tout le cache partagé pour éviter qu'un autre cœur y accède. Mais la méthode entrainerait des performances assez mauvaises. Une autre solution, systématiquement utilisée, est de seulement bloquer les lignes de cache concernées par les écritures en attente. La dernière méthode peut se mettre en œuvre en utilisant judicieusement le protocole de cohérence des caches, en exposant la ''Store Queue'' aux mécanismes de cohérence des caches. ====La mémoire transactionnelle implicite implémentée dans le cache==== Dans cette section, nous allons étudier les techniques qui utilisent le cache pour mémoriser à la fois l'ensemble de lecture et d'écriture. Avant toute chose, précisons ces techniques ont un défaut : la quantité de données manipulées par la transaction est limitée à la taille du cache. Pour éviter ce petit problème, certains chercheurs travaillent sur une mémoire transactionnelle capable de gérer des transactions de taille arbitraires. Ces techniques mémorisent les données modifiées par la transaction en mémoire RAM, dans des enregistrements que l'on appelle des logs, mais passons. Les premières propositions utilisent un cache dédié aux transactions : le '''cache transactionnel'''. À cela, il faut ajouter des instructions pour manipuler le cache transactionnel. Les données dans le cache transactionnel utilisent un protocole légèrement différent des autres caches, avec des états et des transitions en plus. Mais le cout en transistors d'un cache séparé fait que cette méthode n'a jamais été utilisée dans un processeur commercial. Une méthode moins couteuse en circuit réutilise les caches existants dans le processeur. Les écritures se font dans le cache, mais le processeur dispose d'un moyen de les annuler en cas de problème. Mais cela demande de résoudre plusieurs problèmes. Le première est la gestion des ensembles d'écritures/lecture, le second est qu'il faut annuler les écritures si une transaction échoue. Mais un point très intéressant est que la détection des conflits réutilise les mécanismes de cohérence des caches. Si un conflit est détecté pour une ligne de cache, elle est marquée comme invalide pour le protocole de cohérence des caches, et l'invalidation est propagée aux autres cœurs grâce à la liste d'adresse précédente. La détection des conflits se fait donc pendant que la transaction s'exécute, les transactions sont annulées dès qu'un conflit est détecté. Commençons par le premier problème, à savoir la détermination des ensembles de lecture/écriture. Pour cela, chaque ligne de cache possède un bit WRITE qui indique qu'elle a été écrite par une transaction. Les bits READ et WRITE sont mis à 0 au début de chaque transaction. La première écriture dans une ligne de cache met le bit WRITE à 1. Les bits READ et WRITE sont utilisés pour détecter les conflits, la détection des conflits étant prise en charge par les mécanismes de cohérence des caches. [[File:Hardware Transaction.png|centre|vignette|upright=2|Hardware Transaction]] Le second problème à résoudre est que les écritures réalisées lors d'une transaction écrasent une ligne de cache, elles en modifient le contenu. Mais si la transaction est annulée, alors il faut retrouver la ligne de cache originelle, il faut la remettre dans son état d'avant la transaction. Pour cela, la solution la plus simple est que les lignes de cache modifiées ne sont pas écrasées : leur contenu est envoyé en mémoire RAM, elles sont évincées du cache. Il faut juste configurer le cache pour qu'il sauvegarde les lignes de cache modifiées avant de faire les écritures, et ce uniquement lors des transactions. Une autre solution n'envoie pas les données en mémoire RAM, mais profite de la présence d'une hiérarchie de cache, avec la présence d'un cache partagé. Typiquement, les transactions modifient les données dans le cache L1, mais une copie de la donnée originelle est présente dans le cache L2 partagé. Sur les processeur avec un cache L3, c'est ce dernier qui est utilisé pour conserver les données non-modifiées. La raison est que ce dernier est partagé entre tous les cœurs. L'idée est que les transactions modifient les données dans les caches L1/L2 non partagés, mais propagent les écritures dans le cache partagé si la transaction réussit. Si elle échoue, les lignes de caches fautives sont marquées comme invalides par la cohérence des caches. En somme, on utilise des caches inclusifs, mais on débranche l'inclusivité du cache pendant les transactions, avant de les rétablir si la transaction réussit. Les lignes de cache marquées comme lues ou écrites par une transaction (bit READ WRITE) doivent rester dans le cache non-partagé et sont ignorées par l'algorithme de remplacement des lignes de cache. Une autre solution, utilisée sur le processeur Blue gene d'IBM, consiste à avoir plusieurs exemplaires d'une donnée dans le cache, chacun venant d'un processeur/cœur différent. Si un seul processeur a manipulé la donnée partagée, celle-ci ne sera présente qu'en une seule version dans les caches des autres processeurs. Mais si la transaction échoue, alors cela veut dire que plusieurs processeurs ont modifié cette donnée : plusieurs versions d'une même donnée différente sera présente dans le cache Voyons maintenant ce qu'il en est pour la détection des conflits. Plusieurs cas peuvent mener à un conflit et à l'abandon d'une transaction. Les cas en question surviennent quand un processeur veut écrire une donnée utilisée par une autre transaction. * Le premier cas est celui où la donnée n'a pas encore été écrite, elle est dans l'ensemble de lecture. Le premier processeur qui la modifie gagne la course, si on peut dire. Il exécute son écriture, le protocole de cohérence des caches lance une demande d'invalidation des autres copies dans les autres caches, qui fait alors automatiquement échouer les transactions sur les autres processeurs. * Le second cas est celui où la donnée a déjà été modifiée par un processeur dans son cache, mais n'est pas présente dans les autres caches non-partagés. Dans ce cas, si un second processeur veut modifier la donnée, il va aller la chercher dans le cache partagé L2/L3. Mais le protocole de cohérence des caches va remarquer que la donnée est dans le cache L1/L2 d'un autre cœur, avec le bit W mis à 1. Un conflit a alors lieu. ===Le ''speculative Lock Elision''=== Les instructions atomiques sont utilisées de façon pessimistes : l'atomicité est garantie même si aucun autre thread n'accède à la donnée lue/écrite. Aussi, pour accélérer l'exécution des instructions atomiques, des chercheurs ont trouvé une solution à ce problème de réservations inutiles, basée sur la mémoire transactionnelle. L'idée est de générer des transactions en partant des instructions atomiques présentes dans le programme. Une instruction d'acquisition d'un LOCK démarre une transaction, alors qu'une instruction atomique qui le libère termine la transaction. L'optimisation porte le nom de '''Speculative Lock Elision''', abrévié en SLE. Il s'agit d'une méthode que l'on classe à part de la mémoire transactionnelle explicite ou implicite, c'est une troisième possibilité. Pour rappel, une instruction atomique est typiquement une opération de type READ-MODIFY-WRITE : elle lit une donnée, la modifie, et écrit le résultat en mémoire. Tel est le cas de l'instruction FETCH-AND-ADD, ou de ses dérivés. Le SLE n’exécute pas l'instruction atomique permettant d'effectuer un LOCK. À la place, elle exécute une lecture normale, une opération, et l'écriture du résultat. L'écriture n'est pas propagée dans le cache, mais est mise en attente dans la ''Store Queue''. Puis, le processeur exécute les instructions qui suivent de manière transactionnelle. Quand le processeur rencontre une instruction atomique pour libérer le LOCK, il termine la transaction et vérifie si elle s'est bien passée ou doit être annulée. Les écritures en attente dans la ''store queue'' sont alors soit annulées, soit envoyées au cache et disponibles pour le mécanisme de cohérence des caches. Dans sa version non-optimisée, le SLE exécute la transaction une seule fois. Si elle échoue, l'instruction atomique est exécutée pour de vrai. Pour plus d'efficacité, certains processeurs cherchent à éviter ce genre de situation en estimant la probabilité que le premier essai (la transaction) échoue. Pour cela, ils incorporent un circuit permettant d'évaluer les chances que le premier essai marche en tant que transaction : le ''Transaction Predictor''. Une fois cette situation connue, le processeur décide ou non d’exécuter ce premier essai en tant que transaction. ===L'exemple avec le x86=== Dans cette section, nous allons étudier les premiers processeurs grand public qui ont supporté la mémoire transactionnelle matérielle : les processeurs Intel basés sur l’architecture Haswell, sortis aux alentours de mars 2013. Sur ces processeurs, deux modes sont disponibles pour la mémoire transactionnelle matérielle : le mode TSX, et le mode HLE. Le mode TSX fournit quelques instructions supplémentaires pour gérer la mémoire transactionnelle matérielle. On trouve ainsi trois nouvelles instructions : XBEGIN, XEND et XABORT. XBEGIN démarre une transaction, XEND la termine, XABOUT la fait échouer immédiatement. L'instruction XBEGIN fournit une adresse qui pointe sur un morceau de code permettant de gérer l'échec de la transaction. Les registres modifiés par une transaction échouée sont remis dans leur état initial, à une exception près : le registre EAX est utilisé pour retourner un code d'erreur qui indique les raisons de l'échec d'une transaction. Le mode HLE est celui de ''Speculative Lock Elision''. Les instructions atomiques peuvent être transformées en transaction à une condition : qu'on leur rajoute un préfixe. Le préfixe d'une instruction x86 est un octet optionnel, placé au début de l'instruction, qui permet de modifier le comportement de l'instruction. Le préfixe LOCK rend certaines instructions atomiques, REPNZE répète certaines instructions tant qu'une condition est requise, etc. Le fait est que certains préfixes n'ont pas de signification pour certaines instructions et étaient totalement ignorés par le processeur. Pour supporter le Lock Elision, ces préfixes sans significations sont réutilisés pour indiquer qu'une instruction atomique doit subir la Lock Elision. De plus, deux "nouveaux" préfixes font leur apparition : XAQUIRE qui sert à indiquer que l'instruction atomique doit être tentée en tant que transaction ; et XRELEASE qui dit que la transaction spéculative est terminée. Ainsi, un programme peut être conçu pour utiliser la Lock Elision, tout en fonctionnant sur des processeurs plus anciens, qui ne la supportent pas ! Belle tentative de garder la rétrocompatibilité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les coprocesseurs : FPU et IO | prevText=Les coprocesseurs : FPU et IO | next=Un exemple de jeu d'instruction : l'extension x87 | nextText=Un exemple de jeu d'instruction : l'extension x87 }} </noinclude> 14dqmiqjkekhlgbfa8bvnrkn43wuqag 775124 775123 2026-10-13T02:22:44Z Mewtow 31375 /* L'exemple avec le x86 */ 775124 wikitext text/x-wiki Afin de gérer le partage de la mémoire sans problèmes, chaque processeur doit définir un '''modèle mémoire''', un ensemble de restrictions et de contraintes quant à l'interaction avec la mémoire RAM. La première contrainte garantit que les instructions ne puissent pas être interrompues (ou donnent l'impression de ne pas l'être) : c'est la propriété d''''atomicité'''. Un thread doit utiliser plusieurs instructions successives sur la donnée pour pouvoir en faire ce qu'il veut, et cela peut poser des problèmes si les instructions peuvent être interrompues par une exception ou tout autre chose. Par exemple, il est possible qu'une lecture démarre avant que la précédente soit terminée. De même, rien n’empêche une lecture de finir avant l'écriture précédente et renvoyer la valeur d'avant l'écriture. Prenons un exemple, avec un entier utilisé par plusieurs threads, chaque thread s’exécutant sur un processeur x86. Chaque thread veut l'incrémenter. Seul problème, l'incrémentation n'est pas effectuée en une seule instruction sur les processeurs x86. Il faut en effet lire la donnée, l'augmenter de 1, puis l'écrire. Ce qui fait que l'on peut se retrouver dans la situation illustrée ci-dessous, où un processeur n'a pas eu le temps de finir son incrémentation qu'un autre en a démarré une nouvelle. [[File:Illustration du résultat de deux opérations concurrentes sur la même variable.png|centre|vignette|upright=2|Illustration du résultat de deux opérations concurrentes sur la même variable.]] Pour avoir le bon résultat il y a une seule et unique solution : le processeur qui accède à la donnée doit avoir un accès exclusif à la donnée partagée. Sans cela, l'autre processeur ira lire une version de la donnée pas encore modifiée par le premier processeur. Dans notre exemple, un seul thread doit pouvoir manipuler notre compteur à la fois. Et bien sûr, cette réponse se généralise à presque toutes les autres situations impliquant une donnée partagée. On doit donc définir ce qu'on appelle une '''section critique''' : un morceau de temps durant lequel un thread aura un accès exclusif à une donnée partagée, avec la certitude qu'aucun autre thread ne peut modifier la donnée partagée durant ce temps. Autant prévenir tout de suite, créer de telles sections critiques se base sur des mécanismes mêlant le matériel et le logiciel. Il existe deux grandes solutions, qui peuvent être soit codées sous la forme de programmes, soit implantées directement dans le silicium de nos processeurs. L''''exclusion mutuelle''' permet à un thread de réserver la donnée partagée. Un thread qui veut manipuler une donnée réserve celle-ci, et la libère une fois qu'il en a fini avec elle. Si la donnée est réservée, tous les autres threads attendent leur tour. Pour mettre en œuvre cette réservation/dé-réservation, on ajoute un compteur à la donnée partagée, qui indique si la donnée partagée est libre ou déjà réservée. Dans le cas le plus simple, ce compteur vaudra 0 si la donnée est réservée, et 1 si elle est libre. Ce compteur ce qu'on appelle un verrou d'exclusion mutuelle, aussi appelé ''mutex'' (raccourci du terme anglais '''''mut'''ual '''ex'''clusion''). [[File:Mutex.png|centre|vignette|upright=2.5|Mutex.]] ==Les instructions atomiques== Dans le cas précédent, la vérification et modification du compteur ne peut pas être interrompue, sous peine de problèmes. On peut reprendre l'exemple du dessus pour l'illustrer. Si notre compteur est à 0, et que deux threads veulent lire et modifier ce compteur simultanément, il est possible que les deux threads lisent le compteur en même temps : ils liront alors un zéro, et essayeront alors de se réserver la donnée simultanément. Bref, retour à la case départ... ===Les instructions atomiques, niveau jeu d'instruction=== Idéalement, il faudrait que lecture et écriture se fassent en une seule fois. Pour régler ce problème, certains processeurs fournissent des instructions spécialisées, in-interruptibles, capables d'effectuer cette modification du compteur en une seule fois. Elles peuvent lire le compteur, décider si on peut le modifier, et écrire la bonne valeur sans être dérangé par un autre processeur qui viendrait s'inviter dans la mémoire sans autorisation ! Par exemple, sur les processeurs x86, la vérification/modification du compteur vue plus haut peut se faire avec l'instruction ''test and set''. Des instructions similaires existent pour résoudre ce genre de problèmes. Leur rôle est toujours d'implémenter des verrous d'exclusion mutuelle plus ou moins sophistiqués, comme des sémaphores, des verrous (''Locks''), etc. Elles sont appelées des '''instructions atomiques'''. De telles instructions empêchent tout accès mémoire tant qu'elles ne sont pas terminées. Généralement, un programmeur n'a pas à manipuler des instructions atomiques lui-même, mais manipule des abstractions basées sur ces instructions atomiques, fournies par des bibliothèques ou son langage de programmation. Voici la plupart de ces instructions atomiques les plus connues : {|class="wikitable" |- ! Instruction !! Description |- ! Compare And Swap | Cette instruction prend deux opérandes : une utilisée pour une comparaison, une autre à écrire en mémoire RAM. Elle lit une adresse, compare la donnée lue au premier opérande, et copie le second opérande à la même adresse si la comparaison échoue. La comparaison vérifie si la donnée lue et l'opérande sont égales, l'écriture a lieu si elles ne le sont pas. |- ! Fetch And Add | ''Fetch And Add'' incrémente un compteur en mémoire RAM en une seule fois. Elle effectue une lecture du compteur, son incrémentation et l'écriture du résultat en une seule instruction. Elle permet de réaliser ce qu'on appelle des sémaphores ou des compteurs concurrents. |- ! XCHG | Elle échange le contenu d'un registre et d'une adresse de façon atomique. Elle est notoirement utilisée sur les processeurs x86 de nos PC. |} ===Les instructions atomiques, niveau matériel=== L'implémentation la plus simple demande que les processeurs partagent un bus mémoire et aient des caches dédiés. Lors de l’exécution de l'instruction atomique, aucun processeur ne peut aller manipuler la mémoire. L'instruction atomique réserve l'accès au bus et seul le processeur peut accèder à la mémoire tant que l'instruction atomique n'est pas terminée. La réservation se fait en configurant un bit du bus mémoire, appelé le bit LOCK. D'où le terme de '''''bus lock''''' utilisé pour cette solution. Le cout de ce blocage de la mémoire est assez lourd, ce qui rend les instructions atomiques assez lentes. Une solution alternative bloque une ligne de cache, afin qu'aucun autre cœur ne puisse lire ou écrire la ligne de cache réservée. La réservation utilise le protocole de cohérence des caches. Un CPU réserve une ligne de cache en la mettant en état ''Exclusive'' dans son cache. Il suffit alors de maintenir cet état durant la durée de l'instruction pour avoir une instruction atomique. Dans ce cas, pas besoin de bloquer la mémoire, les autres processeurs peuvent faire des lectures et écritures sans problème, tant qu'elles se font dans d'autres lignes de cache. Le coût des instructions atomiques est alors fortement amorti. Il faut noter que la donnée lue/écrite peut se situer à cheval entre deux lignes de cache. C'est ce qu'on appelle un '''''split lock'''''. La première demandent de réserver deux lignes de cache, mais il faut éviter un tas de problèmes tordus du style : un coeur réserve la première ligne de cache, un autre réserve la seconde. Une autre solution bascule sur un ''bus lock'' pour ce genre d'accès non-aligné, mais elle ne marche que sur les processeurs avec un bus partagé. Leur implémentation sur les processeurs x86 modernes est assez mal connue, vu qu'ils n'utilisent pas de bus partagé. * Pour plus d'informations, notamment des mesures expérimentales, allez lire cet article : [https://chipsandcheese.com/p/investigating-split-locks-on-x86 Investigating Split Locks on x86-64]. Sur un processeur avec désambiguïsation mémoire de type x86, il faut attendre que la file d'écriture soit vidée avant de démarrer une instruction atomique. La raison est que les instructions atomiques font une lecture, une opération, et une écriture, dans cet ordre. En théorie, la lecture peut passer avant une écriture précédente, à une adresse différente. Mais dans ce cas, cela signifierait que l'écriture aussi serait déplacée avant, car la lecture est liée à l'écriture les deux se font de manière atomique. Et faire passer une écriture avant une autre n'est pas compatible avec les règles du ''total store ordering''. ==Les instructions LL/SC== Une autre technique de synchronisation est basée sur les instructions '''Load-Link''' et '''Store-Conditional'''. L'instruction Load-Link lit une donnée depuis la mémoire de façon atomique. L'instruction Store-Conditional écrit une donnée chargée avec Load-Link, mais uniquement à condition que la copie en mémoire n'aie pas été modifiée entre temps. Si ce n'est pas le cas, Store-conditional échoue. Pour indiquer un échec, il y a plusieurs solutions : soit elle met un bit du registre d'état à 1, soit elle écrit une valeur de retour dans un registre. Sur certains processeurs, l’exécution d'interruptions ou d'exceptions matérielles après un Load-Link fait échouer un Store-conditional ultérieur. Implémenter ces deux instructions est assez simple, et peut se faire en utilisant les techniques de ''bus-snopping'' vues dans le chapitre sur la cohérence des caches. Pour implémenter l'instruction SC, il suffit de mémoriser si la donnée lue par l'instruction LL a été invalidée depuis la dernière instruction LL. Pour cela, on utilise un registre qui mémorise l'adresse lue par l'instruction LL, à laquelle on ajoute un bit d'invalidation qui dit si cette adresse a été invalidée. L'instruction LL va initialiser le registre d'adresse avec l'adresse lue, et le bit est mis à zéro. Une écriture a lieu sur le bus, des circuits vérifient si l'adresse écrite est identique à celle contenue dans le registre d'adresse et mettent à jour le bit d'invalidation. L'instruction SC doit vérifier ce bit avant d'autoriser l'écriture. ==La mémoire Transactionnelle Matérielle== La '''mémoire transactionnelle''' permet de rendre atomiques des morceaux de programmes complets. Les morceaux de programmes rendus atomiques sont appelés des ''transactions''. Pendant qu'une transaction s'exécute, tout se passe comme si la transaction ne modifiait pas de données et restait plus ou moins "invisible" des autres processeurs. Une fois terminée, le processeur vérifie s'il y a eu un conflit d'accès avec les autres processeurs. Si c'est le cas, la transaction échoue et doit reprendre depuis le début : les changements effectués par la transaction ne seront pas pris en compte. Mais s'il n'y a pas eu conflit d'accès, alors la transaction a réussi et elle peut écrire son résultat en mémoire. Définir une transaction demande d'ajouter plusieurs instructions : une pour démarrer une transaction, une autre pour la terminer, éventuellement une troisième pour annuler précocement une transaction. Lorsqu'une transaction échoue, elle laisse la main au logiciel. Précisément, elle fait un branchement vers une fonction qui gère l'échec de la transaction. La fonction décide s'il faut ré-exécuter la transaction, attendre un peu avant de la re-démarrer, ou faire autre chose. Un autre point est que quand une transaction échoue, les registres doivent être remis à leur valeur initiale, celle d'avant la transaction. Et cette restauration est déléguée au code de gestion d'échec de transaction. Il est aussi possible de gérer la sauvegarde des registres en matériel, avec un système de ''checkpoints'', déjà abordé dans le chapitre sur les processeurs à émission dans l'ordre, dans la section sur les interruptions et le pipeline. Reste à détecter les conflits d'accès. ===La mémoire transactionnelle explicite : la réservation des lignes de cache=== La mémoire transactionnelle se décline en deux versions principales, que nous allons qualifier d'explicite et d'implicite. La mémoire transactionnelle implicite est la plus simple conceptuellement : les lectures et écritures utilisées dans la transaction sont des instructions d'accès mémoire normales, mais sont gérées de manière transactionnelle lors de la transaction. Une autre solution utilise des '''accès mémoires transactionnels explicites''', qui ajoute des instructions d'accès mémoire spécialisées pour les transactions. Il est possible d'exécuter des instructions mémoire normale dans une transaction, qui s'exécutent même si la transaction échoue. Les instructions LOAD/STORE transactionnelles sont les seules à être annulées si la transaction échoue. L'instruction de lecture transactionnelle réserve une ligne de cache pour le processeur. Par réserver, on veut dire que le processeur est le seul à avoir accès en écriture à cette ligne de cache. Si un autre processeur tente d'écrire dans cette ligne de cache, la transaction fautive est annulée. Il est aussi possible de libérer une ligne de cache réservée avec une instruction RELEASE, complémentaire des instructions de lecture/réservation. Pour résumer, il y a donc trois instructions mémoire transactionnelles : READ/RESERVE, WRITE-IF-RESERVED, et RELEASE. Vous aurez peut-être fait le lien avec les instructions LINK-LOAD et STORE-CONDITIONNAL, et il faut avouer que les instructions mémoire transactionnelles ressemblent un petit peu. Disons que ce sont des variantes adaptées au fait qu'elles s'exécutent dans des transactions. De plus, LINK-LOAD et STORE-CONDITIONNAL ne réservent pas des lignes de cache, elles se contentent de vérifier qu'une écriture n'a pas eu lieu entre la lecture et l'écriture. Pour gérer les réservations, le processeur incorpore un '''registre de réservation''' par ligne de cache réservée. Le registre mémorise la donnée lue ou écrite. Les écritures se font dans ce registre, elles ne sont pas propagées dans le cache. Par contre, il est possible que ces registres soient pris en compte par les mécanismes de cohérence des caches, afin de gérer la détection des conflits. Les transactions explicites sont plus flexibles, mais posent plus de problèmes d'implémentation que les transactions implicites. La réservation des lignes de cache est compliquée à implémenter. En comparaison, les méthodes implicites sont plus simples à comprendre et à implémenter. Nous allons nous concentrer sur les méthodes implicites dans le reste du chapitre. ===La mémoire transactionnelle implicite : les ensembles de lecture et d'écriture=== Toute transaction lit un ensemble de données appelé l'<nowiki/>'''ensemble de lecture'',''''' et écrit/modifie des données qui forment l'<nowiki/>'''ensemble d'écriture'''. Le processeur doit identifier l'ensemble de lecture et d'écriture quelque part pour détecter les conflits. Deux conditions font qu'une transaction peut échouer. La première est quand un autre processeur a écrit une donnée dans l'ensemble de lecture. La seconde est quand un autre processeur a lu ou écrit une donnée dans l'ensemble d'écriture. Par contre, il n'y a pas de problème si une donnée est lue par un autre processeur dans l'ensemble de lecture. Mémoriser l'ensemble de lecture est assez simple : il suffit d'ajouter un bit READ à chaque ligne de cache, qui indique qu'elle a été lue par une transaction. Par contre, la gestion de l'ensemble d'écriture est plus complexe. Il y a deux méthodes : une qui autorise les écritures dans le cache, une autre qui met en attente les écritures. ====L'implémentation avec un ensemble d'écritures dans la ''Store Queue''==== Une première méthode met en attente les écritures, qui ne sont propagées dans le cache qu'une fois que la transaction est terminée. Elle mémorise l'ensemble de lecture dans le cache, mais l'ensemble d'écriture reste dans la ''Store Queue'', la ''Load-Store Queue'' ou autre. La technique en question a été implémentée pour la première fois sur les processeurs Transmetta Crusoe et Efficeon, sous le nom de '''''gated store buffer'''''. Avec cette technique, les données sont bel et bien lues depuis le cache, mais les écritures ne sont pas propagées dans le cache, elles restent dans le pipeline. À la fin de la transaction, le processeur vérifie s'il n'y a pas eu de conflits et si la transaction est validée. Si ce n'est pas le cas, la ''Store Queue''/''Load-Store Queue'' est vidée, ce qui annule les changements effectués par la transaction (ROB ou autre méthode capable de gérer les interruptions précises). Si c'est le cas, les écritures sont propagées dans le cache. Si la transaction réussit, le processeur doit vider la ''Store Queue'' dedans. Les écritures se font une par une, ce qui peut prendre un peu de temps, surtout si la transaction a fait beaucoup d'écritures. Et pendant le temps, les données écrites peuvent en théorie être écrasées par une écriture provenant d'un autre cœur. Pour éviter cela, le processeur peut interdire les écritures dans tout le cache partagé pour éviter qu'un autre cœur y accède. Mais la méthode entrainerait des performances assez mauvaises. Une autre solution, systématiquement utilisée, est de seulement bloquer les lignes de cache concernées par les écritures en attente. La dernière méthode peut se mettre en œuvre en utilisant judicieusement le protocole de cohérence des caches, en exposant la ''Store Queue'' aux mécanismes de cohérence des caches. ====La mémoire transactionnelle implicite implémentée dans le cache==== Dans cette section, nous allons étudier les techniques qui utilisent le cache pour mémoriser à la fois l'ensemble de lecture et d'écriture. Avant toute chose, précisons ces techniques ont un défaut : la quantité de données manipulées par la transaction est limitée à la taille du cache. Pour éviter ce petit problème, certains chercheurs travaillent sur une mémoire transactionnelle capable de gérer des transactions de taille arbitraires. Ces techniques mémorisent les données modifiées par la transaction en mémoire RAM, dans des enregistrements que l'on appelle des logs, mais passons. Les premières propositions utilisent un cache dédié aux transactions : le '''cache transactionnel'''. À cela, il faut ajouter des instructions pour manipuler le cache transactionnel. Les données dans le cache transactionnel utilisent un protocole légèrement différent des autres caches, avec des états et des transitions en plus. Mais le cout en transistors d'un cache séparé fait que cette méthode n'a jamais été utilisée dans un processeur commercial. Une méthode moins couteuse en circuit réutilise les caches existants dans le processeur. Les écritures se font dans le cache, mais le processeur dispose d'un moyen de les annuler en cas de problème. Mais cela demande de résoudre plusieurs problèmes. Le première est la gestion des ensembles d'écritures/lecture, le second est qu'il faut annuler les écritures si une transaction échoue. Mais un point très intéressant est que la détection des conflits réutilise les mécanismes de cohérence des caches. Si un conflit est détecté pour une ligne de cache, elle est marquée comme invalide pour le protocole de cohérence des caches, et l'invalidation est propagée aux autres cœurs grâce à la liste d'adresse précédente. La détection des conflits se fait donc pendant que la transaction s'exécute, les transactions sont annulées dès qu'un conflit est détecté. Commençons par le premier problème, à savoir la détermination des ensembles de lecture/écriture. Pour cela, chaque ligne de cache possède un bit WRITE qui indique qu'elle a été écrite par une transaction. Les bits READ et WRITE sont mis à 0 au début de chaque transaction. La première écriture dans une ligne de cache met le bit WRITE à 1. Les bits READ et WRITE sont utilisés pour détecter les conflits, la détection des conflits étant prise en charge par les mécanismes de cohérence des caches. [[File:Hardware Transaction.png|centre|vignette|upright=2|Hardware Transaction]] Le second problème à résoudre est que les écritures réalisées lors d'une transaction écrasent une ligne de cache, elles en modifient le contenu. Mais si la transaction est annulée, alors il faut retrouver la ligne de cache originelle, il faut la remettre dans son état d'avant la transaction. Pour cela, la solution la plus simple est que les lignes de cache modifiées ne sont pas écrasées : leur contenu est envoyé en mémoire RAM, elles sont évincées du cache. Il faut juste configurer le cache pour qu'il sauvegarde les lignes de cache modifiées avant de faire les écritures, et ce uniquement lors des transactions. Une autre solution n'envoie pas les données en mémoire RAM, mais profite de la présence d'une hiérarchie de cache, avec la présence d'un cache partagé. Typiquement, les transactions modifient les données dans le cache L1, mais une copie de la donnée originelle est présente dans le cache L2 partagé. Sur les processeur avec un cache L3, c'est ce dernier qui est utilisé pour conserver les données non-modifiées. La raison est que ce dernier est partagé entre tous les cœurs. L'idée est que les transactions modifient les données dans les caches L1/L2 non partagés, mais propagent les écritures dans le cache partagé si la transaction réussit. Si elle échoue, les lignes de caches fautives sont marquées comme invalides par la cohérence des caches. En somme, on utilise des caches inclusifs, mais on débranche l'inclusivité du cache pendant les transactions, avant de les rétablir si la transaction réussit. Les lignes de cache marquées comme lues ou écrites par une transaction (bit READ WRITE) doivent rester dans le cache non-partagé et sont ignorées par l'algorithme de remplacement des lignes de cache. Une autre solution, utilisée sur le processeur Blue gene d'IBM, consiste à avoir plusieurs exemplaires d'une donnée dans le cache, chacun venant d'un processeur/cœur différent. Si un seul processeur a manipulé la donnée partagée, celle-ci ne sera présente qu'en une seule version dans les caches des autres processeurs. Mais si la transaction échoue, alors cela veut dire que plusieurs processeurs ont modifié cette donnée : plusieurs versions d'une même donnée différente sera présente dans le cache Voyons maintenant ce qu'il en est pour la détection des conflits. Plusieurs cas peuvent mener à un conflit et à l'abandon d'une transaction. Les cas en question surviennent quand un processeur veut écrire une donnée utilisée par une autre transaction. * Le premier cas est celui où la donnée n'a pas encore été écrite, elle est dans l'ensemble de lecture. Le premier processeur qui la modifie gagne la course, si on peut dire. Il exécute son écriture, le protocole de cohérence des caches lance une demande d'invalidation des autres copies dans les autres caches, qui fait alors automatiquement échouer les transactions sur les autres processeurs. * Le second cas est celui où la donnée a déjà été modifiée par un processeur dans son cache, mais n'est pas présente dans les autres caches non-partagés. Dans ce cas, si un second processeur veut modifier la donnée, il va aller la chercher dans le cache partagé L2/L3. Mais le protocole de cohérence des caches va remarquer que la donnée est dans le cache L1/L2 d'un autre cœur, avec le bit W mis à 1. Un conflit a alors lieu. ===Le ''speculative Lock Elision''=== Les instructions atomiques sont utilisées de façon pessimistes : l'atomicité est garantie même si aucun autre thread n'accède à la donnée lue/écrite. Aussi, pour accélérer l'exécution des instructions atomiques, des chercheurs ont trouvé une solution à ce problème de réservations inutiles, basée sur la mémoire transactionnelle. L'idée est de générer des transactions en partant des instructions atomiques présentes dans le programme. Une instruction d'acquisition d'un LOCK démarre une transaction, alors qu'une instruction atomique qui le libère termine la transaction. L'optimisation porte le nom de '''Speculative Lock Elision''', abrévié en SLE. Il s'agit d'une méthode que l'on classe à part de la mémoire transactionnelle explicite ou implicite, c'est une troisième possibilité. Pour rappel, une instruction atomique est typiquement une opération de type READ-MODIFY-WRITE : elle lit une donnée, la modifie, et écrit le résultat en mémoire. Tel est le cas de l'instruction FETCH-AND-ADD, ou de ses dérivés. Le SLE n’exécute pas l'instruction atomique permettant d'effectuer un LOCK. À la place, elle exécute une lecture normale, une opération, et l'écriture du résultat. L'écriture n'est pas propagée dans le cache, mais est mise en attente dans la ''Store Queue''. Puis, le processeur exécute les instructions qui suivent de manière transactionnelle. Quand le processeur rencontre une instruction atomique pour libérer le LOCK, il termine la transaction et vérifie si elle s'est bien passée ou doit être annulée. Les écritures en attente dans la ''store queue'' sont alors soit annulées, soit envoyées au cache et disponibles pour le mécanisme de cohérence des caches. Dans sa version non-optimisée, le SLE exécute la transaction une seule fois. Si elle échoue, l'instruction atomique est exécutée pour de vrai. Pour plus d'efficacité, certains processeurs cherchent à éviter ce genre de situation en estimant la probabilité que le premier essai (la transaction) échoue. Pour cela, ils incorporent un circuit permettant d'évaluer les chances que le premier essai marche en tant que transaction : le ''Transaction Predictor''. Une fois cette situation connue, le processeur décide ou non d’exécuter ce premier essai en tant que transaction. ===L'exemple avec le x86=== Dans cette section, nous allons étudier les premiers processeurs grand public qui ont supporté la mémoire transactionnelle matérielle : les processeurs Intel basés sur l’architecture Haswell, sortis aux alentours de mars 2013. Sur ces processeurs, deux modes sont disponibles pour la mémoire transactionnelle matérielle : le mode TSX, et le mode HLE. Le mode TSX fournit quelques instructions supplémentaires pour gérer la mémoire transactionnelle matérielle. On trouve ainsi trois nouvelles instructions : XBEGIN, XEND et XABORT. XBEGIN démarre une transaction, XEND la termine, XABOUT la fait échouer immédiatement. L'instruction XBEGIN fournit une adresse qui pointe sur un morceau de code permettant de gérer l'échec de la transaction. Les registres modifiés par une transaction échouée sont remis dans leur état initial, à une exception près : le registre EAX est utilisé pour retourner un code d'erreur qui indique les raisons de l'échec d'une transaction. Le mode HLE est celui de ''Speculative Lock Elision''. Les instructions atomiques peuvent être transformées en transaction à une condition : qu'on leur rajoute un préfixe. Le préfixe d'une instruction x86 est un octet optionnel, placé au début de l'instruction, qui permet de modifier le comportement de l'instruction. Le préfixe LOCK rend certaines instructions atomiques, REPNZE répète certaines instructions tant qu'une condition est requise, etc. Le fait est que certains préfixes n'ont pas de signification pour certaines instructions et étaient totalement ignorés par le processeur. Pour supporter le Lock Elision, ces préfixes sans significations sont réutilisés pour indiquer qu'une instruction atomique doit subir la Lock Elision. De plus, deux "nouveaux" préfixes font leur apparition : XAQUIRE qui sert à indiquer que l'instruction atomique doit être tentée en tant que transaction ; et XRELEASE qui dit que la transaction spéculative est terminée. Ainsi, un programme peut être conçu pour utiliser la Lock Elision, tout en fonctionnant sur des processeurs plus anciens, qui ne la supportent pas ! Belle tentative de garder la rétrocompatibilité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La cohérence des caches | prevText=La cohérence des caches | next=Les coprocesseurs : FPU et IO | nextText=Les coprocesseurs : FPU et IO }} </noinclude> ln4gw5pu3a7sks3sshcbxz9bce3yrb4 Fonctionnement d'un ordinateur/Sommaire 0 69596 775120 775006 2026-10-13T02:21:40Z Mewtow 31375 /* Annexes */ 775120 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=== * [[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=== * [[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]] ===L’exécution dans l'ordre et dans le désordre=== * [[Fonctionnement d'un ordinateur/L'émission des instructions|L'émission des instructions]] * [[Fonctionnement d'un ordinateur/Les pipelines in-order|Les pipelines in-order]] * [[Fonctionnement d'un ordinateur/Le scoreboard et le ROB|Le scoreboard et le ROB]] * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Les schedulers et stations de réservation|Les schedulers et stations de réservation]] * [[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]] ===Les processeurs superscalaires=== * [[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]] ===Le parallélisme d'instruction explicite=== * [[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 processeurs x86=== * [[Fonctionnement d'un ordinateur/Les premiers processeurs Intel|Les premiers processeurs Intel]] * [[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]] ==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== * [[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}} 4edbjcn8w3v2uci78fxadsps4n0weq2 775121 775120 2026-10-13T02:21:49Z Mewtow 31375 /* L'architecture externe */ 775121 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]] * [[Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87|Un exemple de jeu d'instruction : l'extension x87]] ===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=== * [[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=== * [[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]] ===L’exécution dans l'ordre et dans le désordre=== * [[Fonctionnement d'un ordinateur/L'émission des instructions|L'émission des instructions]] * [[Fonctionnement d'un ordinateur/Les pipelines in-order|Les pipelines in-order]] * [[Fonctionnement d'un ordinateur/Le scoreboard et le ROB|Le scoreboard et le ROB]] * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Les schedulers et stations de réservation|Les schedulers et stations de réservation]] * [[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]] ===Les processeurs superscalaires=== * [[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]] ===Le parallélisme d'instruction explicite=== * [[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 processeurs x86=== * [[Fonctionnement d'un ordinateur/Les premiers processeurs Intel|Les premiers processeurs Intel]] * [[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]] ==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== * [[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}} 9676mhdrchvp3e2204174opogkej70f Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87 0 71280 775119 763774 2026-10-13T02:20:56Z Mewtow 31375 775119 wikitext text/x-wiki Dans ce chapitre, nous allons étudier le support des nombres flottants sur l'architecture x86 32 bits. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l''''extension x87''', celle qui a ajouté le support des nombres flottants au x86. L'extension x87 était utilisée par défaut sur les PC 32 bits. Depuis l'introduction du jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. Sur les tout premiers processeurs x86, le support du x87 n'était pas implémenté dans le processeur. Il était délégué à des coprocesseurs, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Par la suite, les processeurs x86 ont intégré une FPU qui exécutait les instructions x87 nativement, les coprocesseurs sont devenus inutiles. Nous verrons en détail ces coprocesseurs dans le prochain chapitre. Pour le moment, nous allons juste parler du jeu d'instruction x87. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est 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 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 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 |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. 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 instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de '''double arrondi'' (''double rounding''). [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les sections critiques et le modèle mémoire | prevText=Les sections critiques et le modèle mémoire | next=Les coprocesseurs : FPU et IO | nextText=Les coprocesseurs : FPU et IO }} </noinclude> f27zh7k4gllexxgrmc4v2wm1w5q871h 775125 775119 2026-10-13T02:23:12Z Mewtow 31375 /* Le phénomène de double arrondi */ 775125 wikitext text/x-wiki Dans ce chapitre, nous allons étudier le support des nombres flottants sur l'architecture x86 32 bits. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l''''extension x87''', celle qui a ajouté le support des nombres flottants au x86. L'extension x87 était utilisée par défaut sur les PC 32 bits. Depuis l'introduction du jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. Sur les tout premiers processeurs x86, le support du x87 n'était pas implémenté dans le processeur. Il était délégué à des coprocesseurs, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Par la suite, les processeurs x86 ont intégré une FPU qui exécutait les instructions x87 nativement, les coprocesseurs sont devenus inutiles. Nous verrons en détail ces coprocesseurs dans le prochain chapitre. Pour le moment, nous allons juste parler du jeu d'instruction x87. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est 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 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 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 |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. 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 instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de '''double arrondi'' (''double rounding''). [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les interruptions et exceptions | prevText=Les interruptions et exceptions | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> akdal2it8s3kqrpjwy1i25np39nyo5p Fonctionnement d'un ordinateur/Les interruptions et exceptions 0 80572 775126 744646 2026-10-13T02:23:34Z Mewtow 31375 /* Les identifiants de processus intégrés au processeur */ 775126 wikitext text/x-wiki Les '''interruptions''' sont des fonctionnalités du processeur qui ressemblent beaucoup aux appels de fonctions, mais avec quelques petites différences. Les interruptions, comme leur nom l'indique, interrompent temporairement l’exécution d'un programme pour effectuer un sous-programme nommé '''routine d'interruption'''. Lorsqu'un processeur exécute une interruption, celui-ci : * arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et ''program counter'') ; * exécute la routine d'interruption ; * restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était. [[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]] L'appel d'une routine d'interruption est très similaire à un appel de fonction et implique les mêmes chose : sauvegarder les registres du processeur, l'adresse de retour, etc. Tout ce qui a été dit pour les fonctions marche aussi pour les interruptions. La différence est que la routine d'interruption appartient au système d'exploitation ou à un pilote de périphérique, mais pas au programme en cours d'exécution. Les interruptions sont classées en trois types distincts, aux utilisations très différentes : les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Les deux premières sont des interruptions générés par un évènement extérieur au programme, alors que les interruptions logicielles sont déclenchées quand le programme éxecute une instruction précise pour s'interrompre lui-même, afin d'éxecuter du code appartenant au système d'exploitation ou à un pilote de périphérique. ==Les interruptions et les exceptions matérielles == Les exceptions matérielles et les interruptions matérielles permettent de réagir à un événement extérieur : communication avec le matériel, erreur fatale d’exécution d'un programme. Le programme en cours d'exécution est alors stoppé pour réagir, avant d'en reprendre l'exécution. Elles sont initiés par un évènement extérieur au programme, contrairement aux interruptions logicielles. [[File:Interrupt Routine (Diagramm).png|centre|vignette|upright=2|Déroulement d'une interruption.]] ===Les exceptions matérielles=== Une '''exception matérielle''' est une interruption déclenchée par un évènement interne au processeur, par exemple une erreur d'adressage, une division par zéro... Le processeur intègre des circuits qui détectent l'évènement déclencheur, ainsi que des circuits pour déclencher l'exception matérielle. Prenons l'exemple d'une exception déclenchée par une division par zéro : le processeur doit détecter les divisions par zéro. Lorsqu'une exception matérielle survient, la routine exécutée corrige l'erreur qui a été la cause de l'exception matérielle, et prévient le système d'exploitation si elle n'y arrive pas. Elle peut aussi faire planter l'ordinateur, si l'erreur est grave, ce qui se traduit généralement par un écran bleu soudain. Pour donner un exemple d'utilisation, sachez qu'il existe une exception matérielle qui se déclenche quand on souhaite exécuter une instruction non-reconnue par le processeur. Rappelons que les instructions sont codées par des suites de bits en mémoire, codées sur quelques octets. Mais cela ne signifie pas que toutes les suites de bits correspondent à des instructions : certaines suites ne correspondent pas à des instructions et ne sont pas reconnues par le processeur. Dans ce cas, le chargement dans le processeur d'une telle suite de bit déclenche une exception matérielle "Instruction non-reconnue". Et cela a été utilisé pour émuler des instructions sur les nombres flottants sur des processeurs qui ne les géraient pas. Autrefois, à savoir il y a une quarantaine d'années, les processeurs n'étaient capables d'utiliser que des nombres entiers et aucune instruction machine ne pouvait manipuler de nombres flottants. On devait alors émuler les calculs flottants par une suite d'instructions machine effectuées sur des entiers. Cette émulation était effectuée soit par une bibliothèque logicielle, soit par le système d'exploitation par le biais d'exceptions matérielles. Pour cela, on modifiait la routine de l'exception "Instruction non-reconnue" de manière à ce qu'elle reconnaisse les suites de bits correspondant à des instructions flottantes et exécute une suite d'instruction entière équivalente. ===Les interruptions matérielles=== Les '''interruptions matérielles''', aussi appelées IRQ, sont des interruptions déclenchées par un périphérique ou un circuit extérieur au processeur. Elles sont soit générées par un circuit sur la carte mère, soit par un périphérique, l'essentiel est qu'elles proviennent de l'extérieur du processeur et ne sont pas d'origine logicielle. L'exemple d'utilisation typique des interruptions matérielles est la gestion de certains périphériques. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. La routine d'interruption est alors fournie par le pilote du périphérique. Du moins, c'est comme ça sur le matériel moderne, les anciens PC utilisaient des routines d'interruption fournies par le BIOS. Ce sont celles qui vont nous intéresser dans le chapitre sur la communication avec les périphériques, mais nous n'en parlerons pas dans le détail avant quelques chapitres. Un autre exemple est la gestion des ''timers''. Par exemple, imaginons que vous voyiez à un jeu vidéo, et qu'il vous reste 2 minutes 45 secondes pour sortir d'un laboratoire de recherche avant que l'auto-destruction ne s'active. La durée de 2 minutes 45 est programmée dans un ''timer'', un circuit compteur qui permet de compter une durée. Le jeu vidéo programme le ''timer'' pour qu'il compte durant 2 minutes 45 secondes, puis attend que ce dernier ait finit de compter. Une fois la durée atteinte, le ''timer'' déclenche une interruption, pour stopper l'exécution du jeu vidéo. La routine d'interruption prévient le système d'exploitation que le ''timer'' a fini de compter, le jeu vidéo est alors prévenu, et fait ce qu'il a à faire. Un autre exemple, qui n'est plus d'actualité, est le rafraichissement mémoire des DRAM sur quelques anciens ordinateurs. Dans l'ancien temps, le rafraichissement mémoire était géré par le processeur, pas par le contrôleur mémoire. La plupart des processeurs intégraient des optimisations pour gérer le rafraichissement mémoire par eux-même, sans recourir à des interruptions. Mais quelques ordinateurs ont tenté de relier des processeurs très simples à une mémoire DRAM, directement, alors que les processeurs n'avaient aucune optimisation du rafraichissement mémoire. La gestion du rafraichissement était alors gérée via des interruptions : tous les x millisecondes, un ''timer'' déclenchait une interruption de rafraichissement mémoire, qui rafraichissait la mémoire ou une adresse précise. Cette solution était très peu performante. ==Les interruptions logicielles== Les '''interruptions logicielles''' sont différentes des deux précédentes dans le sens où elles ne sont pas déclenchées par un évènement extérieur. A la place, elles sont déclenchées par un programme en cours d'exécution, via une instruction d'interruption. On peut les voir comme des appels de fonction un peu particuliers, si ce n'est que la routine d'interruption exécutée n'est pas fournie par le programme exécuté, mais par le système d'exploitation, un pilote de périphérique ou le BIOS. Le code éxecuté ne fait pas partie du programme éxecuté, mais en est extérieur, et cela change beaucoup de choses. Sur les PC anciens, le BIOS fournissait les routines de base et le système d'exploitation se contentait d’exécuter les routines fournies par le BIOS. Mais de nos jours, les routines d'interruptions du BIOS sont utilisées lors du démarrage de l'ordinateur, mais ne sont plus utilisées une fois le système d'exploitation lancé. Le système d'exploitation fournit ses propres routines et n'a pas plus besoin des routines du BIOS. ===Les interruptions du BIOS et des autres firmwares=== Le BIOS fournit des routines d'interruption pour gérer les périphériques et matériels les plus courants. Il y a une interruption pour communiquer avec le port série RS232 de notre ordinateur, une autre pour le port parallèle, une autre pour le clavier, une autre pour la carte graphique, et quelques autres. Les interruptions en questions gèrent des standards de base, utilisés pour gérer les périphériques et matériels les plus courants. Par exemple, tant que la carte graphique supporte le standard VGA, le BIOS peut l'utiliser, bien que partiellement et seulement pour gérer les fonctions de base. idem avec le clavier : les standards PS/2 et USB sont gérés de base par le BIOS. Ce n'est pas pour rien que « BIOS » est l'abréviation de Basic Input Output System, ce qui signifie « programme basique d'entrée-sortie ». Par exemple, si aucune ROM vidéo n'est détectée, le BIOS peut quand même communiquer directement avec la carte graphique en utilisant une interruption dédiée. Elle a plusieurs utilités différentes, mais est généralement limitée à l’affichage de texte (la carte graphique est gérée en mode texte). Dans ce mode, elle peut tout aussi bien envoyer du texte à l'écran (sortie) que renvoyer la position du curseur à l'écran (entrée). L'usage de ces standards matériel était extrêmement puissant malgré sa simplicité. Il était possible de créer un OS complet en utilisant juste des appels de routine du BIOS. Par exemple, le DOS, ancêtre de Windows, utilisait exclusivement les interruptions du BIOS ! Mais une fois le système d'exploitation démarré, les interruptions du BIOS ne servent plus, les pilotes de périphériques prennent le relai. De nos jours, l'UEFI fournit encore un équivalent des anciennes interruptions du BIOS, mais seulement pour la rétrocompatibilité. Les interruptions ne sont plus utilisées lors du démarrage de l'ordinateur, le firmware est programmé plus finement et gére le matériel d'une manière autre. Certaines routines peuvent effectuer plusieurs traitements : par exemple, la routine qui permet de communiquer avec le disque dur peut aussi bien lire un secteur, l'écrire, etc. Pour spécifier le traitement à effectuer, on doit placer une certaine valeur dans le registre AH du processeur : la routine est programmée pour déduire le traitement à effectuer uniquement à partir de la valeur du registre AH. Mais certaines routines ne font pas grand-chose : par exemple, l'interruption 0x12h ne fait que lire la taille de la mémoire conventionnelle, qui est mémorisée à un endroit bien précis en mémoire RAM. Voici une description assez succincte de ces routines. Vous remarquerez que je n'ai pas vraiment détaillé ce que font ces interruptions, ni comment les utiliser. Il faut dire que de nos jours, ce n'est pas franchement utile. Mais si vous voulez en savoir plus, je vous invite à lire la liste des interruptions du BIOS de Ralf Brown, disponible via ce lien : [http://www.ctyme.com/rbrown.htm Liste des interruptions du BIOS, établie par Ralf Brown]. {|class="wikitable" |- !Adresse de la routine dans le vecteur d'interruption !Description succinte |- !10h |Si aucune ROM vidéo n'est détectée, le BIOS peut quand même communiquer directement avec la carte graphique grâce à cette routine. Elle a plusieurs fonctions différentes et peut tout aussi bien envoyer un caractère à l'écran que renvoyer la position du curseur. |- !13h |Cette routine du BIOS permet de lire ou d'écrire sur le disque dur ou sur une disquette. Plus précisément, cette routine lui sert à lire les premiers octets d'un disque dur afin de pouvoir charger le système d'exploitation. Elle était aussi utilisée par les systèmes d'exploitation du style MS-DOS pour lire ou écrire sur le disque dur. |- !14h |La routine 14h était utilisée pour communiquer avec le port série RS232 de notre ordinateur. |- !15h |La routine 15h a des fonctions diverses et variées, toutes plus ou moins rattachées à la gestion du matériel. Le BIOS était autrefois en charge de la gestion de l'alimentation de notre ordinateur : il se chargeait de la mise en veille, de réduire la fréquence du processeur, d'éteindre les périphériques inutilisés. Pour cela, la routine 15h était utilisée. Ses fonctions de gestion de l'énergie étaient encore utilisées jusqu'à la création de Windows 95. De nos jours, avec l'arrivée de la norme ACPI, le système d'exploitation gère tout seul la gestion de l'énergie de notre ordinateur et cette routine est donc obsolète. À toute règle, il faut une exception : cette routine est utilisée par certains systèmes d'exploitation modernes à leur démarrage afin d'obtenir une description correcte et précise de l'organisation de la mémoire de l'ordinateur. Pour cela, nos OS configurent cette routine en plaçant la valeur 0x0000e820 dans le registre EAX. |- !16h |La routine 16h permet de gérer le clavier et de le configurer. Cette routine est utilisée tant que le système d'exploitation n'a pas démarré, c'est pour cela que vous pouvez utiliser le clavier pour naviguer dans l'écran de configuration de votre BIOS. En revanche, aucune routine standard ne permet la communication avec la souris : il est impossible d'utiliser la souris dans la plupart des BIOS. Certains BIOS possèdent malgré tout des routines capables de gérer la souris, mais ils sont très rares. |- !17h |Cette routine permet de communiquer avec une imprimante sur le port parallèle de l'ordinateur. Comme les autres, on la configure avec le registre AH. |- !19h |Cette routine est celle qui s'occupe du démarrage du système d'exploitation. Elle sert donc à lancer le système d'exploitation lors du démarrage d'un ordinateur, mais elle sert aussi en cas de redémarrage. |} Les routines du BIOS étaient parfois recopiées dans la mémoire RAM afin de rendre leur exécution plus rapide. Certaines options du BIOS, souvent nommées '''''BIOS memory shadowing''''', permettent justement d'autoriser ou d'interdire cette copie du BIOS dans la RAM. ===Les appels systèmes des systèmes d'exploitation=== [[File:Operating system placement-fr.svg|vignette|Différence entre le système d'exploitation et les applications.]] Avant de poursuivre, rappelons que le '''système d'exploitation''' sert d'intermédiaire entre les autres logiciels et le matériel. Les programmes ne sont pas censés accéder d'eux-mêmes au matériel, pour des raisons de portabilité et de sécurité. Ils ne peuvent pas accéder directement au disque dur, au clavier, à la carte son, etc. À la place, ils demandent au système d'exploitation de le faire à leur place et de leur transmettre les résultats. Il y a donc une séparation stricte entre : * les programmes systèmes qui gèrent la mémoire et les périphériques ; * les programmes applicatifs ou applications, qui délèguent la gestion de la mémoire et des périphériques aux programmes systèmes. Les programmes systèmes sont en réalité des sous-programmes, des fonctions utilisées pour accéder à la carte graphique, manipuler la mémoire, gérer des fichiers, etc. Les fonctions en question sont exécutées en faisant des appels de fonction classiques, appelés des '''appels système'''. Par exemple, linux fournit les appels systèmes open, read, write et close pour manipuler des fichiers, ou encore les appels brk, sbrk, pour allouer et désallouer de la mémoire. Évidemment, ceux-ci ne sont pas les seuls : linux fournit environ 380 appels systèmes distincts. Les appels systèmes permettent aux programmes d'exécuter des fonctions pré-programmées, qui agissent sur le matériel. La communication entre OS et programmes est donc standardisée, limitée par une interface, ce qui limite les problèmes de sécurité et simplifie la programmation des applications. Les appels systèmes sont un concept des systèmes d'exploitation, qui peuvent se mettre en œuvre de plusieurs manières. On peut les implémenter de plusieurs manières différentes, mais ils sont presque toujours des interruptions logicielles. Les programmes systèmes sont le plus souvent des routines d'interruptions, fournies par l'OS ou les pilotes de périphérique. Un appel système n'est donc qu'une interruption logicielle qui exécute à la demande la routine adéquate. Pour simplifier, l'ensemble de ces routines d'interruption porte le nom de '''noyau du système d'exploitation'''. Il regroupe les programmes systèmes, qu'on peut appeler avec des appels système. Le noyau d'un OS est la partie de l'OS qui s'occupe de la gestion du matériel, des périphériques et des opérations demandant de reconfigurer le processeur. Mais sans intervention du matériel, rien n’empêche à un programma applicatif de lire ou d'écrire dans les registres des périphériques, par exemple. Mais les processeurs utilisent des sécurités pour cela, que nous allons voir dans ce qui suit. ==Les niveaux de privilège (anneaux mémoire)== Nous venons juste de voir que les interruptions logicielles sont surtout utilisées pour manipuler un périphérique, accéder au matériel. Et ce qu'elles soient fournies par le BIOS ou le système d'exploitation. C'est une différence fondamentale entre interruption logicielle et simple appel de fonction. Les interruptions peuvent manipuler le matériel, mais du code normal en est incapable. La raison tient à une sécurité incorporée dans tous les systèmes modernes : les '''niveaux de privilèges''', aussi appelés des '''anneaux mémoires'''. Les niveaux de privilèges varient grandement d'un jeu d'instruction à l'autre, mais il y en a au minimum deux : le '''mode noyau''' et le '''mode utilisateur'''. Le mode noyau est réservé au noyau du système d'exploitation, le mode utilisateur est réservé aux autres logiciels. En plus des modes utilisateur et noyau, les processeurs gèrent souvent un autre mode appelé '''mode système''', réservé au firmware/BIOS. D'autres modes sont parfois présents sur les processeurs modernes, avec des intermédiaires entre mode noyau et utilisateur. Pour résumer : le mode utilisateur est réservé aux logiciels applicatifs, le mode noyau est réservé au noyau du système d'exploitation (d'où son nom), le mode système est réservé au firmware/BIOS. Suivant le mode de fonctionnement, certaines opérations sensibles sont interdites. Par exemple, l'accès aux périphériques est interdit en mode utilisateur, mais autorisé dans le mode noyau. De même, certaines instructions sont autorisées seulement en mode noyau. Par exemple, les instructions permettant de configurer le processeur, en changeant ses registres de contrôle, sont accessibles seulement en mode noyau et en mode système. L'avantage est que cela empêche les programmes d’accéder directement au matériel, seul le système d'exploitation et le BIOS en sont capables. Généralement, le mode utilisateur est le plus limité, le mode système permet absolument tout, le mode noyau est assez proche du mode système avec quelques limitations en plus. Les anneaux mémoire/niveaux de privilèges étaient initialement gérés par des mécanismes purement logiciels, mais sont actuellement gérés par le processeur. Pour cela, le registre de contrôle du processeur contient un bit qui précise si le programme en cours est en mode noyau, utilisateur, hyperviseur ou système. À chaque accès mémoire ou exécution d'instruction, le processeur vérifie si le niveau de privilège permet l'opération demandée. Lorsqu'un programme effectue une instruction interdite pour le mode en cours, une exception matérielle est levée. Généralement, le programme est arrêté sauvagement et un message d'erreur est affiché. ===Les interruptions basculent en mode noyau/système=== L'ordinateur démarre généralement en mode système, puis il bascule en mode hyperviseur, puis en mode noyau, et enfin en mode utilisateur. Il peut revenir vers un mode antérieur sous certaines conditions. Et justement, toute interruption bascule automatiquement le processeur dans l'espace noyau, voire système. C'est une nécessité pour les interruptions logicielles, afin de passer d'un programme en espace utilisateur à une routine qui est en espace noyau. Les interruptions matérielles doivent aussi faire la transition en espace noyau ou système, car l'accès au matériel n'est pas possible en espace utilisateur. Par exemple, une interruption qui fait passer du mode utilisateur vers le noyau permet à un logiciel de déléguer une tâche au noyau du système d'exploitation. De même, une interruption qui fait passer du mode noyau vers le mode système permet à l'OS de communiquer avec le firmware, de déléguer une fonction vers le BIOS. Il s'agit là d'une sécurité : le passage d'un mode à un autre est contrôlé et n'est autorisé qu'en utilisant des instructions très précises, en l’occurrence des interruptions. Le passage en mode noyau n'est cependant pas gratuit, de même que l'interruption qui lui est associée. Ainsi, les interruptions sont généralement considérées comme lentes, très lentes. Elles sont beaucoup plus lentes que les appels de fonction normaux, qui sont beaucoup plus simples. Les raisons à cela sont multiples, mais la principale est la suivante : les mémoires caches doivent être vidés lors des transferts entre mode noyau et mode utilisateur. Alors attention : diverses optimisations font que seuls certains caches spécialisés dont nous n'avons pas encore parlé, comme les TLB, doivent être vidés. Mais malgré tout, cela prend beaucoup de temps. ===Le mode noyau et le mode utilisateur : logiciels et OS=== [[File:Most common protection rings.svg|vignette|Espace noyau et utilisateur.]] Tous les processeurs des PC modernes (x86 64 bits) gèrent au moins deux niveaux de privilèges : un ''mode noyau'' pour le noyau de l'OS et un ''mode utilisateur'' pour les applications. Tout est autorisé en mode noyau, alors que le mode utilisateur ne peut pas accéder aux périphériques, ni gérer certaines portions protégées de la mémoire. C'est un mécanisme qui force à déléguer la gestion du matériel au système d'exploitation. Le mode utilisateur n'a pas accès à certaines instructions importantes, appelées des '''instructions privilégiées''', qui ne s'exécutent qu'en espace noyau. Elles regroupent les instructions pour accéder aux entrées-sorties et celles pour configurer le processeur. On peut considérer qu'il s'agit d'instructions que seul l'O.S peut utiliser. À côté, on trouve des '''instructions non-privilégiées''' qui peuvent s’exécuter aussi bien en mode noyau qu'en mode utilisateur. Si un programme tente d'exécuter une instruction privilégiée en espace utilisateur, le processeur considère qu'une erreur a eu lieu et lance une exception matérielle. De plus, l'espace utilisateur restreint l'accès à la mémoire par divers mécanismes dits de '''protection mémoire''', alors que le mode noyau n'a pas de restrictions. Un programme en mode utilisateur se voit attribuer une certaine portion de la mémoire RAM, et ne peut accéder qu'à celle-ci. En clair, les programmes sont isolés les uns des autres : un programme ne peut pas aller lire ou écrire dans la mémoire d'un autre, les programmes ne se marchent pas sur les pieds, les bugs d'un programme ne débordent pas sur les autres programmes, etc. La séparation en mode noyau et utilisateur explique pourquoi les appels systèmes sont implémentés avec des interruptions, et non des appels de fonction basiques. La raison est qu'un appel système branche vers un programme système en espace noyau, alors que le programme qui lance l'appel système est en espace utilisateur. Même sans accès aux périphériques, le passage en mode noyau est nécessaire pour passer outre la protection mémoire. La routine de l'appel système est dans une portion de mémoire réservée à l'OS, auquel le programme exécutant n'a pas accès en espace utilisateur. Il en est de même pour certaines structures de données du système d'exploitation, accessibles seulement dans l'espace noyau. Or, les appels de fonction et branchements ne permettent pas de passer de l'espace utilisateur à l'espace noyau, alors que les interruptions le font automatiquement. Vu qu'une interruption logicielle est assez lente, divers processeurs incorporent des techniques pour rendre les appels systèmes plus rapides, en remplaçant les interruptions logicielles par des instructions spécialisées (SYSCALL/SYSRET et SYSENTER/SYSEXIT d'AMD et Intel). D'autres techniques similaires tentent de faire la même chose, à savoir changer le niveau de privilège sans utiliser d'interruptions : les ''call gate'' d’Intel, les ''Supervisor Call instruction'' des IBM 360, etc. Ce qui fait qu'assimiler interruptions logicielles et appels systèmes est en soi une erreur, mais même si les deux sont très liés. Quelques processeurs ont des registres d'état séparés pour le mode noyau et le mode utilisateur. Le registre d'état du mode noyau ne peut être consulté que si le processeur est en mode noyau, l'autre registre d'état est consultable à la fois en mode noyau et utilisateur. ===Le mode système pour les firmwares=== Dans le '''mode de gestion système''', dans lequel l'exécution du système d'exploitation est suspendue et laisse la main au ''firmware''. Il est possible d’entrer dans ce mode en utilisant une interruption spéciale, appelée ''System Management Interrupt'' (SMI ). Il est utilisé pour la gestion de l'énergie de l'ordinateur, la gestion thermique, pour gérer des interruptions non-masquables, pour gérer des défaillances matérielles graves, pour gérer certains périphériques (émuler les claviers/souris PS/2, certaines fonctionnalités USB/Thunderbolt), éventuellement pour communiquer avec la puce TPM du ''chipset''. Sur les CPU x86, il s'appelle le ''System Management Mode'', abrévié SMM. Il permet à l'OS de laisser la main au BIOS pour exécuter des interruptions spécifiques. Il a été rendu disponible sur les CPU 386, et a ensuite été utilisé pour faciliter l'implémentation du standard APM, un standard de gestion de l'énergie assez ancien qui a laissé sa place à l'ACPI. ACPI qui utilisait aussi ce mode dans ses premières implémentations et l'utilise encore sur certaines cartes mères. Par sécurité, ce mode utilise un espace d'adressage différent de celui utilisé par l'OS afin de garantir un minimum de protection mémoire. Ce qui empêche pas certains malwares d'utiliser le mode SMM pour faire leur travail, voire se cacher de l'OS. Il faut noter que le passage en mode système se fait en mode noyau, mais n'est pas disponible en mode utilisateur. Il s'agit d'une sécurité, qui garantit que les logiciels n'ont pas accès aux interruptions du firmware/BIOS directement. Le système d'exploitation peut communiquer avec le BIOS, pour que ce dernier l'aide à gérer le matériel. Par exemple, le système d'exploitation peut demander au BIOS quels sont les périphériques installés sur l'ordinateur. L'OS peut ainsi savoir quelle est la carte graphique ou la carte son installée, il a juste à demander au BIOS. Mais un logiciel utilisateur n'est pas censé pouvoir faire ça, seul le noyau est censé le faire. ===Les modes intermédiaires pour les pilotes de périphériques=== [[File:Priv rings.svg|droite|vignette|Niveaux de privilèges sur les processeurs x86.]] Sur certains processeurs, on trouve des niveaux de privilèges intermédiaires entre l'espace noyau et l'espace utilisateur. Les processeurs x86 des PC 32 bits contiennent 4 niveaux de privilèges. Le système Honeywell 6180 en possédait 8, de même que le Multics system original. À l'origine, ceux-ci ont été inventés pour faciliter la programmation des pilotes de périphériques. Mais force est de constater que ceux-ci ne sont pas vraiment utilisés, seuls les espaces noyau et utilisateur étant pertinents. Sur PC, les 4 niveaux de privilèges étaient autrefois utilisés pour la virtualisation. Les anciens processeurs x86 n'avaient pas de mode hyperviseur. Alors à la place, le noyau du système d'exploitation était placé dans le second niveau de privilège, celui juste après le mode noyau. La majorité des opérations de l'OS étaient possibles dans ce mode, sauf quelques unes qui requéraient le mode noyau. L'hyperviseur émulait ces interruptions qui demandaient le mode noyau en fournissant ses routines à lui, conçues pour gérer la virtualisation. L'ajout d'un réel mode hyperviseur a changé la donne. Sur les processeurs Data General Eclipse MV/8000, les modes disposaient chacun de zones mémoires séparées. Les trois bits de poids fort du ''program counter'' étaient utilisés pour déterminer le niveau de privilége. Le processeur était un processeur 32 bits, ce qui fait que les 4 gibioctets de RAM adressables étaient découpés en 8 blocs de 512 mébioctets. Le code exécuté avait son niveau de privilège qui dépendait du bloc de 512 mébioctet dans lequel il était. Tout branchement qui modifiait les 3 bits de poids fort du ''program counter'' entrainait automatiquement un changement de niveau de privilège. ==L'implémentation des interruptions== Toutes les interruptions, qu'elles soient logicielles ou matérielles, ne s'implémentent pas exactement de la même manière. Mais certaines choses sont communes à toutes les interruptions et à toutes leurs mises en œuvre. Par exemple, on s'attend à ce que la majeure partie des processeurs qui supportent les interruptions disposent des fonctionnalités que nous allons voir dans ce qui suit, à savoir : un vecteur d'interruption, une pile dédiée aux interruptions, la possibilité de désactiver les interruptions, etc. Elles ne sont pas tout le temps présentes, mais leur absence est plus une exception que la régle. ===Le vecteur d'interruption=== Vu le grand nombre d'interruptions logicielles/appels système, on se doute bien qu'il y a a peu-près autant de routines d'interruptions différentes. Et celles-ci sont placées à des endroits différents en mémoire RAM. Appeler une interruption demande techniquement de connaitre son adresse, pour effectuer un branchement vers celle-ci. Mais comment déterminer son adresse ? La solution la plus simple est de placer chaque routine d'interruption systématiquement au même endroit en mémoire, ce qui fait que l'adresse est connue à l'avance. Les appels systèmes sont alors des appels de fonctions basiques, avec un branchement inconditionnel vers une adresse fixe. La technique marche bien pour les interruptions du firmware, comme celles du BIOS, qui sont placées en ROM à une position fixe. Mais elle est trop contraignante dès qu'un système d'exploitation est impliqué. Fixer la position et la taille de chaque routine d'interruption ne marche pas si on ne connait pas à l'avance ni le nombre, ni la taille, ni la fonction des routines. Pour résoudre ce problème, les systèmes d'exploitation modernes font autrement. Ils numérotent les interruptions, à savoir qu'ils leur attribuent un numéro en commençant par 0. Un PC X86 moderne gère 256 interruptions, numérotées de 0 à 255. Un appel système ne précise pas l'adresse vers laquelle faire un branchement, mais précise le numéro de l'interruption à exécuter. Le système d'exploitation s'occupe ensuite de retrouver l'adresse de la routine à partir du numéro de l'interruption. Pour cela, le système d'exploitation mémorise une table de correspondance qui associe chaque numéro à l'adresse de l'interruption. La table de correspondance s'appelle le '''vecteur d'interruption'''. Par exemple, la dixième adresse de la table pointe vers la dixième interruption, à savoir l'interruption qui gère le disque dur ou le lecteur de disquette. Il s'agit plus précisément un tableau d'adresses, à savoir que les adresses de chaque interruption sont placées dans un bloc de mémoire, les unes à la suite des autres. Le vecteur d'interruption mémorise les adresses pour toutes les routines, sans exceptions. Non seulement il mémorise celles des appels systèmes, mais aussi les routines des exceptions matérielles, ainsi que les routines des interruptions matérielles. Sur les PC modernes, le vecteur d'interruption est stocké dans les 1024 premiers octets de la mémoire. Il gère 256 interruptions, et les 32 premières sont réservées aux exceptions matérielles. : Pour ceux qui connaissent la programmation, le vecteur d'interruption est un tableau de pointeurs sur fonction, les fonctions étant les routines à exécuter. L'avantage est que l'adresse de la routine n'a pas à être précisée lors de la conception de l'OS, et elle peut même changer lors de l'exécution d'un programme ! Le vecteur d'interruption peut être mis à jour, les adresses changées, ce qui permet de remplacer à la volée les routines d'interruptions utilisées. Une adresse qui pointe vers telle routine peut être remplacée par une autre adresse qui pointe vers une autre routine. On dit qu'on '''déroute''' ou qu'on '''détourne''' le vecteur d'interruption. Tous les systèmes d'exploitation modernes le font après le démarrage de l'ordinateur, pour remplacer les interruptions du BIOS par les interruptions fournies par le système d'exploitation et les pilotes. Le vecteur d'interruption est placé en mémoire RAM est initialisé au démarrage de l'ordinateur. Il est initialisé avec les adresses des routines du Firmware, à savoir les routines du BIOS ou de l'UEFI sur les PCs. Mais une fois que le système d'exploitation démarré, les adresses sont mises à jour pour pointer vers les routines du système d'exploitation et des pilotes de périphériques. Cette mise à jour est effectuée par le système d'exploitation, une fois que le BIOS lui a laissé les commandes. L'usage d'un vecteur d'interruption permet donc une plus grande flexibilité et une compatibilité maximale. Elle permet au système d'exploitation de configurer les interruptions comme il le souhaite et de placer les routines d'interruption où il veut. Par contre, elle a un léger cout en performance, très mineur. La raison est que déterminer l'adresse d'une routine d'interruption se fait en deux temps, au lieu d'un. Au lieu de faire un branchement vers une adresse connue à l'avance, on doit récupérer l'adresse dans le vecteur d'interruption, puis faire le branchement. Il y a un accès mémoire en plus, il y a un niveau d'indirection en plus. Cela explique pourquoi un appel système n'est pas qu'un simple appel de fonction, pourquoi il est préférable d'avoir une instruction spécifique pour le processeur, séparée de l'instruction d'appel de fonction normale. La conversion d'un numéro d'interruption en adresse peut se faire au niveau matériel ou logiciel. S'il est fait au niveau matériel, l'instruction d'interruption logicielle lit l'adresse automatiquement dans le vecteur d'interruption, idem avec les exceptions matérielles et les IRQ. Avec la solution logicielle, on délègue ce choix au système d'exploitation. Dans ce cas, le processeur contient un registre qui stocke le numéro de l'interruption, ou du moins de quoi déterminer la cause de l'interruption : est-ce le disque dur qui fait des siennes, une erreur de calcul dans l'ALU, une touche appuyée sur le clavier, etc. ===Le masquage d'interruptions : désactiver les interruptions=== Il est possible de désactiver temporairement l’exécution des interruptions, quelle qu’en soit la raison. Le terme utilisé n'est pas désactivation des interruption, mais '''masquage des interruptions'''. Le masquage d'interruption permet de bloquer des interruptions temporairement, pour soit les ignorer, soit les exécuter ultérieurement. La désactivation peut-être totale ou partielle : totale quand toutes les interruptions sont désactivées, partielle quand seule une minorité l'est. Le registre de contrôle, qui permet de configurer le processeur, incorpore souvent un bit qui permet d'activer/désactiver les interruptions de manière globale. En modifiant ce bit, on peut activer ou désactiver les interruptions. Le bit en question n'est modifiable qu'en mode noyau. D'autres bits du registre de contrôle permettent de désactiver certaines interruptions précises, voir de choisir lesquelles activer/désactiver. Et ce n'est pas le seul, d'autres bits de configuration ne sont modifiables qu'en mode noyau, pas en mode utilisateur. Désactiver les interruptions est utile dans certaines situations assez complexes, notamment quand le système d'exploitation en a besoin. C'est aussi utilisé dans certains systèmes dit temps réels, où les concepteurs ont besoin de garanties assez fortes pour le temps d’exécution. Une contrainte est que chaque fonction doit s’exécuter en un temps définit à l'avance, qu'il ne doit pas dépasser. Par exemple, prenons le cas d'une fonction devant s’exécuter en moins de 300 millisecondes. Le code en question prend 200 ms sans interruption, ce qui fait 100ms de marge de sureté. Si plusieurs interruptions surviennent, les 100ms de marge de sureté peuvent être dépassées. Désactiver les interruptions pendant le temps d’exécution du code permet d'éviter cela. Et à ce petit jeu, il faut distinguer les '''interruptions masquables''' qui peuvent être ignorées ou retardées, des '''interruptions non-masquables''', à savoir des interruptions qui ne doivent pas être masquées, quelle que soit la situation. Le terme "interruption non-masquable" est souvent abrévié en NMI, ce qui signifie ''Non Maskable Interrupt''. Dans ce qui suit, nous parlerons parfois de NMI par abus de langage, pour simplifier l'écriture. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Les défaillances matérielles en question regroupent des situations très variées : une perte de l'alimentation, une erreur de parité mémoire, une surchauffe du processeur, etc. Elles sont généralement détectées par un paquet de circuits dédiés, souvent par des circuits placés sur la carte mère, en dehors d'un contrôleur de périphérique : un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc. Un exemple d'utilisation des interruptions non-masquable est celui d'une surchauffe du processeur. Le processeur et la carte mère contiennent de nombreux capteurs de température, eux-même connectés à des circuits de surveillance. Si la température est trop élevée, les circuits de surveillance déclenchent une interruption non-masquable. La routine d'interruption non-masquable effectue quelques manipulations d'urgence et éteint l'ordinateur par sécurité. Mais elle l'éteint d'une manière assez propre, en faisant quelques manipulations de dernière seconde. Même chose en cas de défaillance de l'alimentation électrique, par exemple lorsqu'on débranche la prise, une coupure de courant, un problème matériel avec les régulateurs de tension, des condensateurs de la carte mère qui fondent, etc. Les ordinateurs modernes peuvent fonctionner durant quelques millisecondes lors d'une défaillance de l'alimentation, parce que la carte mère contient des condensateurs qui maintienne la tension d'alimentation pendant quelques millisecondes. Ce qui lui laisse le temps de faire quelques sauvegardes mineures, comme générer un crash dump, avant d'éteindre l'ordinateur proprement. Outre les défaillances matérielles, les interruptions non-masquables sont aussi utilisée pour la gestion du ''watchdog timer''. Pour rappel, le ''watchdog timer'' est un mécanisme de sécurité qui redémarre l'ordinateur s'il suspecte qu'il a planté. C'est un compteur/décompteur connecté à l'entrée RESET du processeur, pour qu'un débordement d'entier du compteur déclenche un RESET. Pour éviter cela, une interruption non-masquable réinitialise le ''watchdog timer'' régulièrement, avant qu'il déborde. L'interruption est programmée soit par le ''watchdog timer'', soit par un autre ''timer'', peu importe. L'interruption en question doit être non-masquable, car on ne veut pas que l’ordinateur redémarre car l'interruption du ''watchdog timer'' a été masqué pendant trop longtemps, même si le masquage était pertinent. [[File:SimpleWatchdogTimer.gif|centre|vignette|upright=2|Le ''Watchdog Timer'' et l'ordinateur.]] ==Les optimisations des interruptions== En soi, les interruptions sur des appels de fonction améliorés. Les optimisations générales pour les appels de fonction marchent aussi pour les interruptions. Par exemple, les interruptions peuvent profiter du fenêtrage de registres. Lorsqu'une interruption se déclenche, elle se voit allouer sa propre fenêtre de registres, séparée des autres. Cependant, de nombreux processeurs incorporent des optimisations pour accélérer spécifiquement le traitement des interruptions, pas seulement les appels de fonction. Il s'agit souvent de processeurs dédiés à l'embarqué, qui sont peu puissants et doivent consommer peu, et qui doivent communiquer avec un grand nombre d'entrée-sorties. Les optimisations en question fournissent des registres dédiés aux interruptions, parfois une pile d'appel dédiée. ===Les registres dédiés aux interruptions=== Sur certains processeurs, les registres généraux sont dupliqués en deux ensembles identiques. Le premier ensemble est utilisé pour exécuter les programmes normaux, alors que le second ensemble est dédié aux interruptions. Mais les noms de registres sont identiques dans les deux ensembles. L'ensemble de registres dédié aux interruptions regroupe les '''registres d'interruption'''. Ce système permet de simplifier grandement la gestion des interruptions matérielles. Lors d'une interruption sur un processeur sans registres d'interruption, l'interruption doit sauvegarder les registres qu'elle manipule, pour ne pas perturber le programme interrompu. Avec des registres d'interruption, il n'y a pas besoin de sauvegarder les registres lors d'une interruption, car les registres normaux et les registres d'interruption ne sont en réalité pas les mêmes. Les interruptions sont alors plus rapides. Notons qu'avec ce système, seuls les registres adressables par le programmeur sont dupliqués. Les registres comme le pointeur de pile ou le ''program counter'' ne sont pas dupliqués, car ils n'ont pas à l'être. Et attention : certains registres doivent être sauvegardés par l'interruption. Notamment, l'adresse de retour, qui permet de reprendre l'exécution du programme interrompu au bon endroit. La sauvegarde de l'adresse de retour sur la pile est réalisée automatiquement par le processeur. ===La pile dédiée aux interruptions=== La plupart des systèmes d'exploitation utilisent une pile d'appel dédiée, séparée, pour les interruptions. Les raisons à cela sont multiples. La principale est que sur les systèmes d'exploitation capables de gérer plusieurs programmes en même temps, c'est une solution assez évidente. Chaque programme a sa propre pile d'appel, séparée des autres. Et la routine d'interruption est un programme comme un autre, qui doit donc avoir sa propre pile d'appel. ==Annexe : la multiprogrammation et les commutations de contexte== Les systèmes d’exploitation modernes implémentent la '''multiprogrammation''', le fait de pouvoir lancer plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Il s'agit de la technique dite du '''multiplexage temporel''', terme bien complexe pour simplement dire : exécution à tour de rôle. Du moins, c'était le fonctionnement avant l'arrivée des processeurs multi-cœurs. Maintenant, des logiciels distincts s’exécutent sur des processeur distincts. Du moins, tant qu'on a moins de logiciels que de cœurs. Si le nombre de cœurs n'est pas suffisant, le multiplexage temporel est utilisé. ===Les commutations de contexte=== Passer d'un programme à un autre est un processus complexe, résumé sous le terme de '''commutation de contexte'''. Une commutation de contexte demande de stopper l'exécution d'un programme pour laisser la main à un autre. Et cela devrait vous rappeler quelque chose : c'est ce qui se fait lors d'une interruption, par exemple. L'idée est de stopper le programme en cours avec une '''interruption de commutation de contexte''', qui donne la main au programme suivant. Pour cela, l'interruption de commutation de contexte agit sur les registres du processeur, au même titre que le fait une interruption ou un appel de fonction. Premièrement, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour va lui restaurer son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres. Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Les processeurs incorporent des instructions pour dire : je veux sauvegarder l'état du processeur dans tel contexte, restaurer celui-ci, etc. Par exemple, les processeurs x86 peuvent faire des commutations de contexte presque totalement en matériel, grâce à des techniques de '''''Hardware Task Switching'''''. Malheureusement, ces techniques ne sont pas utilisées, à cause de quelques défauts. Par exemple, le ''Hardware Task Switching'' des CPU x86 ne sauvegarde pas tous les registres : il oublie les registres flottants ! ===Les identifiants de processus intégrés au processeur=== Sur les processeurs simples, une commutation de contexte se contente de sauvegarder et restaurer les registres. Mais sur les processeurs modernes, ce n'est pas le cas. Une commutation de contexte doit faire d'autres opérations absolument nécessaires. Par exemple, elle doit aussi remettre à zéro les caches, réinitialiser certaines unité matérielles qu'on n'a pas encore abordées (la TLB), et bien d'autres. Pour le moment, nous ne pouvons pas expliquer pourquoi dans le détail, car cela demande de connaitre la mémoire virtuelle, qui est l'objet du chapitre suivant. Mais nous pouvons dire que c'est une question de protection mémoire. L'idée est que si un programme charge une donnée dans le cache, les autres programmes ne doivent pas y avoir accès, même par erreur. Pour simplifier ces opérations supplémentaires, le processeur 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é les '''identifiants de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. Le registre d'identifiant est modifié à chaque changement de processus, à chaque commutation de contexte. L'identifiant de processus CPU est aussi utilisé lors des accès mémoire. Par exemple, il peut être utilisé pour les accès au cache. Les données dans le cache sont concaténées avec l'identifiant du processus qui les chargé dans le cache, ce qui est utile pour la protection mémoire. Sans cela, chaque processus peut en théorie accéder à des données qui ne sont pas à lui dans le cache, en envoyant l'adresse adéquate. Nous en reparlerons dans le chapitre sur les mémoires caches. Un 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. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La pile d'appel et les fonctions | prevText=La pile d'appel et les fonctions | next=Un exemple de jeu d'instruction : l'extension x87 | nextText=Un exemple de jeu d'instruction : l'extension x87 }} </noinclude> dmnwwhd06zd37j137lv2ta8kmgivpce Mathc initiation/a594 0 81090 775054 774882 2026-10-12T16:16:37Z Xhungab 23827 775054 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a522#Analyse IV : Se familiariser avec les séries de Fourier|Sommaire]] [[File:Fourier serie for my wikibook.png|thumb|Fourier serie f(x) = x with x[0..Pi]]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPOQGZMpyjDEHjnf1wsQbDJG&si=mhV8SOb34ohQKvKm Playlist]].''' En analyse mathématique, les séries de Fourier sont un outil fondamental dans l'étude des fonctions périodiques. C'est à partir de ce concept que s'est développée la branche des mathématiques connue sous le nom d'analyse harmonique. [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier wikipedia] . '''Pour les définitions de a0, b0, an et bn j'utilise la [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier#Coefficients_réels Première convention]''' * <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * <math>\mathbf{b_0=0}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> '''Deux propriétés :''' * Si f(t) est '''paire''', alors '''bn = 0'''. * Si f(t) est '''impaire''', alors '''a0 = 0 et an = 0'''. . ---- ==Se familiariser avec les series de Fourier :== Quelques exemples : * [[Mathc initiation/a593|La fonction : f(t) = t ...................... avec t = [0..PI]]] * [[Mathc initiation/007y|La fonction : f(t) = t ...................... avec t = [-PI..PI]]] * [[Mathc initiation/007n|La fonction : f(t) = t^2 .................. avec t = [-PI..PI]]] * [[Mathc initiation/a602|La fonction : f(t) = (t(t+1)) ........... avec t = [-1..1]]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/a9b|Présentation]] * [[Mathc initiation/0080|Un exemple avec atan2();]] {{AutoCat}} j7v4myvei51smehaim4aeg6h9ow6rsy 775130 775054 2026-10-13T08:08:32Z Xhungab 23827 775130 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a522#Analyse IV : Se familiariser avec les séries de Fourier|Sommaire]] [[File:Fourier serie for my wikibook.png|thumb|Fourier serie f(x) = x with x[0..Pi]]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPOQGZMpyjDEHjnf1wsQbDJG&si=mhV8SOb34ohQKvKm Playlist]].''' En analyse mathématique, les séries de Fourier sont un outil fondamental dans l'étude des fonctions périodiques. C'est à partir de ce concept que s'est développée la branche des mathématiques connue sous le nom d'analyse harmonique. [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier wikipedia] . . ==Les series de Fourier :== * <math>\mathbf{S(t) =a_0 + \sum_{n = 1}^{\infty} \left ( a_n \cdot \cos \left ( nt\frac{2\pi}T\right ) + b_n \cdot \sin \left ( nt\frac{2\pi}T\right )\right )}</math>. '''Pour les définitions de a0, b0, an et bn j'utilise la [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier#Coefficients_réels Première convention]''' * <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * <math>\mathbf{b_0=0}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> '''Deux propriétés :''' * Si f(t) est '''paire''', alors '''bn = 0'''. * Si f(t) est '''impaire''', alors '''a0 = 0 et an = 0'''. . ---- ==Se familiariser avec les series de Fourier :== Quelques exemples : * [[Mathc initiation/a593|La fonction : f(t) = t ...................... avec t = [0..PI]]] * [[Mathc initiation/007y|La fonction : f(t) = t ...................... avec t = [-PI..PI]]] * [[Mathc initiation/007n|La fonction : f(t) = t^2 .................. avec t = [-PI..PI]]] * [[Mathc initiation/a602|La fonction : f(t) = (t(t+1)) ........... avec t = [-1..1]]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/a9b|Présentation]] * [[Mathc initiation/0080|Un exemple avec atan2();]] {{AutoCat}} ped83y1jfmjkkkqajlexu6x4ovq8mhe 775131 775130 2026-10-13T08:12:05Z Xhungab 23827 775131 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[File:Fourier serie for my wikibook.png|thumb|Fourier serie f(x) = x with x[0..Pi]]] [[Mathc initiation/a522#Analyse IV : Se familiariser avec les séries de Fourier|Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPOQGZMpyjDEHjnf1wsQbDJG&si=mhV8SOb34ohQKvKm Playlist]].''' En analyse mathématique, les séries de Fourier sont un outil fondamental dans l'étude des fonctions périodiques. C'est à partir de ce concept que s'est développée la branche des mathématiques connue sous le nom d'analyse harmonique. [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier wikipedia] . ==Les series de Fourier :== . * <math>\mathbf{S(t) =a_0 + \sum_{n = 1}^{\infty} \left ( a_n \cdot \cos \left ( nt\frac{2\pi}T\right ) + b_n \cdot \sin \left ( nt\frac{2\pi}T\right )\right )}</math> '''Pour les définitions de a0, b0, an et bn j'utilise la [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier#Coefficients_réels Première convention]''' * <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * <math>\mathbf{b_0=0}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> '''Deux propriétés :''' * Si f(t) est '''paire''', alors '''bn = 0'''. * Si f(t) est '''impaire''', alors '''a0 = 0 et an = 0'''. . ---- ==Se familiariser avec les series de Fourier :== Quelques exemples : * [[Mathc initiation/a593|La fonction : f(t) = t ...................... avec t = [0..PI]]] * [[Mathc initiation/007y|La fonction : f(t) = t ...................... avec t = [-PI..PI]]] * [[Mathc initiation/007n|La fonction : f(t) = t^2 .................. avec t = [-PI..PI]]] * [[Mathc initiation/a602|La fonction : f(t) = (t(t+1)) ........... avec t = [-1..1]]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/a9b|Présentation]] * [[Mathc initiation/0080|Un exemple avec atan2();]] {{AutoCat}} 5671kvnb3j4yqsa43liuotn7xd24ddx Mathc initiation/a593 0 81091 775038 774913 2026-10-12T12:58:24Z Xhungab 23827 775038 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/a594#Se familiariser avec les series de Fourier :| Sommaire]] : : ==La fonction : f(t) = t ...................... avec t = [0..PI]== : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]] : . : ==La fonction f :== * [[Mathc initiation/a595|f.h ]] ...... '''f(t) = t avec t = [0..PI]''' : . : ==Les coefficients de la séries de Fourier :== * [[Mathc initiation/a601|c0a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * [[Mathc initiation/a596|c0an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * [[Mathc initiation/a597|c0bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> . {| class="wikitable" |+ Séries de Fourier |- ! plot [0.:pi] [0.:pi] !! plot [0.:4*pi] [0.:pi] |- | [[File:Fourier serie for my wikibook.png|thumb|]] || [[File:Fourier serie for my wikibook 5.png|thumb]]] |} Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers termes''' de la série. * [[Mathc initiation/a598|c0fs.c]] Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série. * [[Mathc initiation/a615|c0fsN.c]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/a9d|c0fsap.c]] {{AutoCat}} sak4o53o4xi7i4l7c8mte4jvgvvk666 Mathc initiation/a602 0 81099 775041 774750 2026-10-12T13:03:09Z Xhungab 23827 775041 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/a594#Se familiariser avec les series de Fourier :| Sommaire]] : : ==La fonction : f(t) = (t(t+1)) ........... avec t = [-1..1]== : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]] : . : ==La fonction f :== * [[Mathc initiation/a603|f1.h]] ...... '''f(t) = (t(t+1)) avec t = [-1..1]''' : . : ==Les coefficients de la séries de Fourier :== * [[Mathc initiation/a604|c1a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * [[Mathc initiation/a605|c1an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * [[Mathc initiation/a606|c1bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> . {| class="wikitable" |+ Séries de Fourier |- ! plot [-1:1] [-.5:2.] !! plot [-1:4] [-.5:2.] |- | [[File:Fourier serie in language c with gnuplot.png|thumb|]] || [[File:Fourier serie for my wikibook 4.png|thumb|]] |} Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007p|gnuplot]]''' '''les 9 premiers termes''' de la série. * [[Mathc initiation/a607|c1fs.c]] Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série. * [[Mathc initiation/a616|c1fsN.c]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/a9c|c1fsap.c]] {{AutoCat}} rctc17kfhjzro6ekq3d6kia69u36zq7 Fonctionnement d'un ordinateur/Les microarchitectures pour le x86 : haute performance 0 82608 775128 774776 2026-10-13T02:25:28Z Mewtow 31375 /* Un aparté sur la FPU des processeurs x86 */ 775128 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre, car elle est arrivée après la superscalarité. Le jeu d'instruction x86 pose quelques problèmes pour la superscalarité, car c'est un jeu d'instruction CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent.' Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, le ''scheduler'', la file de micro-opération, et les autres structures similaires. Il faut cependant noter qu'avoir des instructions complexes peut avoir des avantages sur les processeurs superscalaires à exécution dans le désordre. Les avantages sont liés au fait que certaines instructions complexes peuvent être implémentées avec des circuits dédiés, plutot qu'avec une suite de µops ou d'instructions. Les performances sont alors meilleures car une grosse instruction complexe devient une seule µops, ce qui aide le renommage de registres, prend moins de place dans le ROB ou les stations de réservation, etc. : Pour avoir une petite idée des avantages et inconvénients, je conseille la lecture de cet articles de blog : [https://chipsandcheese.com/p/why-x86-doesnt-need-to-die Why x86 Doesn’t Need to Die]. Il s'agit d'une réponse à cet autre article, plus court : [https://hackaday.com/2024/03/21/why-x86-needs-to-die/ Why X86 Needs To Die]. ==Introduction : généralités et historique== Avant de voir les processeurs dans un ordre chronologique, il est bon de faire quelques généralités, histoire de bien cadrer ce qui va suivre. Les architectures Intel et AMD se classent en deux types : une classe de microarchitectures pour la haute performance, une classe de microarchitectures moins performante mais plus économe. La première est utilisée sur les PC fixes et parfois sur les serveurs, les processeurs pour ''desktop'' et serveur pouvant être différents. Les microarchitectures pour serveurs sont souvent des variantes des microarchitectures pour ''desktop''. Les serveurs ont surtout besoin d'exécuter un grand nombre d programmes/processus en même temps, ce qui fait qu'elles ont besoin d'un grand nombre de cœurs moins de cœurs performants individuellement. Pour les applications ''desktop'', c'est l'inverse : elles ont besoin de cœurs performants, quitte à en avoir moins. Pour la seconde catégorie, parler de microarchitecture basse consommation serait peu-être un peu exagéré, car elles ne visent pas non les smartphones et autres appareils embarqué. Mais elles le sont suffisamment pour entrer dans un ordinateur portable, où l'autonomie de la batterie est très importante. Dan ce chapitre, nous allons parler exclusivement des processeurs haute performance, les microarchitectures "basse consommation" seront pour le prochain chapitre. ===Les différences et points communs entre µarchs d'Intel et d'AMD=== Les microarchitectures Intel et AMD ne sont pas très différentes quand on regarde de loin. Elles ont évoluées dans la même direction, avec une introduction précoce de la superscalarité, puis de l'exécution dans le désordre, du renommage de registre, de la prédiction de branchement, d'un cache de µops, etc. Néanmoins, il y a des différences entre les deux qui relèvent de la tendance générale, avec de nombreuses exceptions : * Intel préfère utiliser un ''scheduler'' centralisé, alors qu'AMD préfère des stations des ''schedulers'' décentralisés. * AMD sépare les unités flottantes/SIMD des unités entières, elles ont des ports d'émission bien séparés. Intel, en comparaison, préfère mettre une ALU et une unité FPU/SIMD sur le même port d'émission. Mais il s'agit là de tendance, avec de nombreuses exceptions. Par exemple, les microarchitectures K6 et Bulldozer d'AMD utilisaient une station de réservation centralisé. De même, les micraorchitectures récentes d'Intel tendent de plus en plus à scinder le ''scheduler''s, de plus en plus avec le temps. Le fait est qu'elle a dû grossir avec le temps, au point de devoir être scindée tellement elle prenait de circuits. La microarchitecture Skylake l'a scindée en une station de réservation pour les µops mémoire et une autre pour le reste. La microarchitecture ''Sunny Cove'' a ensuite scindée la station de réservation mémoire en deux : une pour les lectures et une pour les écritures. Dans ce chapitre, nous ne parlerons pas des unités de prédiction de branchement, ni de la hiérarchie des caches. Et les raisons sont différentes entre les eux. Les unités de prédiction de branchement ont été discutées dans le chapitre sur la prédiction de branchement. De plus, de nombreux processeurs différents utilisent des unités de prédiction de branchement qui ont des algorithmes similaires. Mais la raison principale est que l'unité de prédiction de branchement est complétement détachée du reste du pipeline, cache d'instruction excepté sur certaines microarchitectures. N'importe quelle unité de branchement peut s'adapter à n'importe quel pipeline/microarchitecture, tant que ses ''itmings'' et délais en cycles d'horloge tiennent la route. Il en est plus ou moins de même pour la hiérarchie de caches. Me système mémoire est assez indépendant du reste de la microarchitecture. Dans ce qui va suivre, vous allez rapidement remarquer quelque chose d'assez évident. Les processeurs x86 ont en quelque sorte deux "pipelines" séparés, assez différents les unes des autres, qui ont évolués en parallèle. D'un côté, il y a le "pipeline" entier/mémoire, qui exécute les opérations entières et les µops mémoire. Il regroupe des ALU entières, des AGU (unité de calcul d'adresse) et les unités mémoire LOAD/STORE. Il communique directement avec la ''Load/Store Queue'', car c'est là dedans que se font les calculs d'adresse. A l'opposé, les calculs flottants sont réalisés dans des pipelines séparés, sauf sur quelques processeurs qui font exception, comme les microarchitectures Core. Nous verrons pourquoi dans la section suivante. Pour ce qui est des accès mémoire, les microarchitectures Intel et AMD ont évolué différemment. Intel a suivi cette progression : * Le Pentium 1 utilisait une unité mémoire intégrée dans le pipeline entier. * Le Pentium 3 a séparé l'unité mémoire du pipeline entier, mais ne restait capable que d'émettre un accès mémoire par cycle. * La microarchitectures Core a ajouté la possibilité de faire deux émissions mémoire simultanées : une lecture avec une écriture. * Par la suite, de nouveaux ports de lectures ont été rajoutés. ===Un aparté sur la FPU des processeurs x86=== Les processeurs x86 oint acquis une FPU assez rapidement dans l'histoire. Les processeurs 286 et 386 n'avaient pas de FPU, mais pouvaient faire appel à un coprocesseur spécialisé dans les calculs flottants : le '''coprocesseur x87'''. Le coprocesseur x87 intégrait 8 registres flottants et supportait 8quelques instructions flottante, comme FADD pour l'addition, FSUB pour la soustraction, FMUL pour la multiplication, FDIV pour la division, etc. Le coprocesseur x87 a été intégré dans le processeur assez rapidement, dès le 486. Pour cela, le 486 intégrait une FPU et les 8 registres flottants. Les instructions FADD, SUB, FMUL, et quelques autres, ont aussi été ajoutées au jeu d'instruction de base du 486. Elles formaient une '''extension du jeu d'instruction x86''', à savoir des paquets d'instructions qui n'étaient pas dans le jeu d’instruction de base, mais qui ont été rajoutés par la suite et sont devenus des standards. Détail extraordinairement important : le coprocesseur x87 intégrait aussi 8 registres, avec une organisation très spéciale. Les registres n'étaient pas "adressables" avec des noms de registres, ils étaient à la place organisé en une pile d'opérande. Tout cela a été expliqué dans le chapitre "Un exemple de jeu d'instruction : l'extension x87". Et cela posait pas mal de problème pour rendre les opérations flottantes performantes. Notamment, le renommage de registres devait une nécessité. Le fait que les registres flottants et généraux soient séparés fait qu'ils sont souvent renommés à part, dans deux unités e renommage différentes. Les registres flottants ont été les premiers à être renommés. De nombreux processeurs renommaient les registres flottants, alors que les registres généraux n'étaient pas renommés. La raison est que quand on un budget en transistor limité, on ne peut pas renommer les deux. Et autant c'est utile de renommer les registres généraux, autant la pile de registre du x87 rendait cela quasiment nécessaire pour avoir des performances convenables. un autre détail est que les optimisations du renommage de registres sont d'abord arrivées sur le pipeline flottant, avant d'être portées sur le pipeline entier. Un exemple est celui des techniques d'élimination des MOV (''MOV elimination''), qui traitent les instructions MOV entre registres directement dans l'unité de renommage. Ou encore, les techniques avec des idiomes pour mettre à zéro un registre en le Xorant avec lui-même. Elles ont été utilisées d'abord sur les registres flottants/SIMD, avant d'arriver sur les registres généraux. Quelques processeurs les utilsaient pour les registres flottants, mais pas les registres généraux. Il en est de même avec le renommage dans le ROB versus à banc de registre physique. Quelques CPU renommaient les registres flottants avec un renommage à banc de registre physique, avec que le pipeline entier utilisait encore le renommage dans le ROB. Les processeurs Athlon et Phenom d'AMD étaient dans ce cas, par exemple. Chez Intel, le renommage à banc de registre physique est apparu sur le Pentium 4 et a été appliqué sur tous les registres. En général, la FPU est précédée par une station de réservation unique (ou par un ''scheduler'' sur certains CPU). Il y a souvent un port d'émission pour l'additionneur flottant et un autre pour le multiplieurs, sauf pour les tout premiers modèles d'Intel et AMD. Il y aussi souvent un port pour les instructions FSTORE, les écritures flottantes. Les ports en question servent à transmettre les données à écrire vers l'unité mémoire, ainsi que l'adresse mémoire associée. {|class="wikitable" |+ Station de réservation flottante |- ! Station de réservation 1 !! Station de réservation 2 !! Station de réservation 3 |- | FADD (additionneur flottant) || FMUL (multiplication flottante || FSTORE |} ===Un aparté sur les instructions SIMD des processeurs x86=== L'extension flottante x87 est resté la norme pendant une bonne décennie, sur les CPU 32 bits. Mais par la suite, les choses ont changées avec l'introduction des '''instructions SIMD'''. Je ne vais pas rentrer dans le détail, un chapitre ultérieur détaillera les instructions SIMD. Mais je dois cependant parler des bases de ces instructions, car elles ont remplacé les instructions flottantes progressivement. Mais avant toute chose, un petit historique,pour vous faire comprendre le vocabulaire utilisé dans ce qui suit. Les premières instructions SIMD ont été introduites en 1996, sur le Pentium MMX. Les instructions en question étaient regroupées dans une '''extension x86''' (un ajout au jeu d'instruction de base), appelé le MMX. Il ne contenait que des instructions SIMD pour des opérandes entiers. Mais par la suite, les extensions '''''Streaming SIMD Extensions''''' (SSE) ont ajouté des opérations flottantes. Les ''Streaming SIMD Extensions'', qu'on va abrévier SSE dans ce qui suit, ont été déclinées en plusieurs versions : SSE1, SSE2, SEE3, SSE4, et quelques intermédiaires. Puis, vint le '''AVX''' (''Advanced Vector eXtensions''). Les instructions SIMD regroupent plusieurs opérations identiques en une seule instruction, la seule contrainte étant que les opérandes soient différentes. Par exemple, une instruction d'addition SIMD regroupe quatre additions séparées, chacun travaillant sur des opérandes différentes. [[File:Instructions SIMD.png|centre|vignette|upright=3|Instructions SIMD]] J'ai donné l'exemple d'une addition SIMD qui regroupe quatre additions flottantes, travaillant chacune sur ses opérandes rien qu'à elle. Les opérandes en question ne sont pas lues depuis les registres généraux ou flottants. A la place, les instructions utilisent un banc de registre dédié, qui contient des registres assez larges. Les instructions SIMD utilisent donc des '''registres SIMD''', qui mémorisent des paquets d'opérandes. Par exemple, l'extension SSE a ajouté 16 registres XMM, de 128 bits chacun. un registre de 128 bits peut mémoriser : 2 entiers/flottants 64 bits, 4 entiers/flottants 32 bits, 8 entiers 16 bits, 16 entiers 8 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] L'AVX, quant à elle, donne accès à 16 registres de 256 bits chacun. Pour faire des économies de circuits, ils ont été fusionnés avec les registres XMM du SSE. Les 128 bits de poids faible de ces registres ne sont autre que les registres du SSE. [[File:AVX registers.svg|centre|vignette|upright=1.5|Registres AVX.]] Si je parle de ces instructions et registres SIMD, c'est parce qu'ils ont remplacé les instructions/registres flottants d'antan. Le x87 a disparu, les instructions SIMD font office d'instructions flottantes. Le remplacement a eu lieu avec le passage au 64 bits. Les raisons à cela sont multiples, mais la principale est que le x87 était très mal fait, avec une architecture obsolète. Une annexe en parle en détail, mais le x87 utilisait une pile de registre totalement inhabituelle, que les compilateurs ne pouvaient pas utiliser convenablement. Aussi, lors du passage au 64 bits, Intel et AMD en ont profité pour se débarrasser du x87 et le remplacer par quelque chose de plus moderne. Vu que les extensions SIMD faisaient déjà des calculs flottants, elles l'ont remplacé. Et même dans le processeur, dans sa microarchitecture, le remplacement a été total. Le processeur intègre une ou plusieurs unités de calcul spécialisées dans les instructions SIMD. Et elles remplacent la FPU, totalement. Typiquement, il y a au minimum : * une unité SIMD spécialisée dans les additions entières ; * une unité SIMD spécialisée dans les multiplications entières ; * une unité SIMD spécialisée dans les additions flottantes ; * une unité SIMD spécialisée dans les multiplications flottantes. * éventuellement une unité SIMD annexe pour les autres instructions SIMD, et elles sont nombreuses. Les unités SIMD étaient parfois mutualisées avec la FPU, c'est à dire que la FPU x87 était en réalité une sous-partie de l'unité SIMD. Après tout, l'unité SIMD contient plusieurs additionneurs flottants 32/64 bits, idem pour les multiplieurs flottants. Alors autant en profiter pour leur faire faire des calculs flottants normaux, ceux de la FPU x87. ==Les processeurs x86 d'Intel== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Nous allons commencer avec les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont superscalaires, mais sans exécution dans le désordre. L'absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Les autres processeurs x86 sont tous avec à exécution dans le désordre, à une exception qu'on verra dans le chapitre suivant. AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. Ceci étant dit, commençons par le commencement, avec le Pentium 1. ===Le Pentium 1/MMX : la double émission entière avec deux pipelines=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 (Pentium 3) : l'exécution dans le désordre=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec un ''scheduler'' centralisé, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisé, en sortie de l'unité de renommage. Elles avaient 4 ports d'émission. En réalité, 5 dont deux pour les écritures (un pour l'adresse à transmettre, un pour la donnée). Le tout avait une répartition moitié-moitié pour les calculs et les accès mémoire. Chaque port d'émission pour les calculs était relié à une ALU et une FPU/unité SIMD. Le tableau suivant résume tout. Vous remarquerez la présence de plusieurs additionneurs flottants. Le premier exécute les opérations x87, l'autre les opérations SIMD. Les opérations SIMD se font sur des registres de 128 bits, mais elles sont en réalité cassées en deux µops, chacune travaillant sur 64 bits. {|class="wikitable" |+ Pentium 3 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 !! Port d'émission 5 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || LOAD || STORE adresse || STORE donnée |- | LEA || ALU SIMD MMX || || || |- | Multiplieur entier || || || || |- ! rowspan="3" | µops flottantes | x87 FADD || FADD 64 bits (SIMD) || || |- | FMUL (x87 et 64 bits) || || || |- | FDIV || || || |} Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé un ''scheduler'' centralisé, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core : désambiguïsation mémoire, macro-fusion, ''stack engine'', ''Fetch Input Queue''=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. A part ça, les améliorations sont assez diverses : un cache L2, l'arrivé de la désambiguïsation mémoire, une ''Fetch Input Queue'' et un ''stack engine'' primitif. Le processeur introduit aussi la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. beaucoup d'optimisations importantes, donc. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 5 ports d'émission, dont un pour les lectures et un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer'', elle-même reliée au cache de données par un port de lecture et un port d'écriture. Les ports de lecture/écriture du cache font 128 bits, pour gérer naturellement les instructions LOAD/STORE dans les registres SIMD. Les trois ports restants sont chacun reliés à des ALU entières et des unités flottantes/SIMD (pour l'extension SSE). Le multiplieur entier est relié au second port d'émission. L'additionneur flottant SIMD/SSE est connecté au port 1, le multiplieur/diviseur flottant SIMD/SSE est connecté au port 0, une unité SIMD annexe est connectée sur chaque port. {|class="wikitable" |+ Microarchitecture Core (Penryn et Merom) |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 !! Port d'émission 5 |- ! rowspan="3 | µops entières/mémoire | ALU entière || ALU entière || ALU entière || LOAD || STORE |- | ''Barrel shifter'' || Multiplieur entier || ''Barrel shifter'' || || |- |- | || || ALU branchements || || |- ! rowspan="3 | µops flottantes et SIMD | FMUL et FDIV || FADD || || |- | ALU SIMD entière (128 bits) || ALU SIMD entière (128 bits) || ALU SIMD entière (128 bits) || || |- | || MUL SIMD entière (128 bits) || || || |} Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié, avec un mix allant de trois opérations entières à trois opérations flottantes, avec tous les intermédiaires. Le fait de mettre les ALU SIMD sur des ports différents permet d'émettre une addition et une multiplication flottante/SIMD simultanément. ===La microarchitecture Nehalem : un vrai ''Loop Stream Detector''=== Les microarchitectures '''Nehalem''' et '''Westmere''' n'introduisent pas d'optimisation majeure. A la rigueur, ils introduisent une prédiction de branchement à deux niveaux, et une macro-fusion plus complète. Les unités de calcul sont assez semblables, de même que leur répartition sur les ports d'émission. Elle a introduit cependant un vrai ''Loop Stream Detector'' (LSD). Pour rappel, le LSD qui permet d'exécuter les boucles directement depuis la file de µops. Le LSD reconnait les petites boucles, qui tiennent dans la file de µops, et lit les µops adéquates depuis celle-ci. Les instructions de la boucle n'ont donc pas à être chargée et décodées. La microarchitecture Core avait une sorte de pseudo-''Loop Stream Detector'', mais il était localisé dans la file d'instruction. Il était donc moins performant. Ce pseudo-''Loop Stream Detector'' permettait de ne pas charger les instruction x86, mais elles devaient quand même être décodées et passer par la file de µops. Avec Nehalem, ce n'est plus le cas. [[File:Intel Nehalem arch.svg|centre|vignette|upright=2.5|Intel Nehalem.]] ===La microarchitecture Sandy Bridge : le cache de µops=== Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' ont introduit le renommage à banc de registre physique et l'ajout d'un cache de micro-opérations. L'ajout du cache de micro-opérations est une optimisation particulièrement rentable, particulièrement pour le complexe jeu d’instruction x86. Avec ce cache, les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder des instructions en-dehors de boucles, pas plus. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Sur les processeurs modernes, les décodeurs ne servent que pour 10 à 20% des micro-opérations exécutées, le reste est lu depuis le cache de µops. Cependant, Sandy Bridge a un cache de µops assez limité, qui ne permet pas d’atteindre ces chiffres. De nombreuses instructions ne peuvent pas être mémorisées dans le cache de µops, les limitations quand à son utilisation sont assez importantes. Par exemple, le processeur ne consulte pas le cache de µops à chaque cycle, il ne démarre son utilisation qu'après un branchement (donc une boucle potentielle) et cesse de l'utiliser au premier défaut de cache (dans le cache de µops). Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même. Pour le reste, ''Sandy Bridge'' n'innove pas trop. Les unités d'exécution sont globalement les mêmes, leur répartition sur les port est peu changée. LA seule différence intéressante est que ''Sandy Bridge'' n'a plus une unité mémoire dédiée aux LOAD et une aux STORE. Elle en a deux qui peuvent faire indifféremment LOAD et STORE. Le processeur peut donc émettre deux LOAD simultanément, ce qui entraine un gain en performance appréciable. ===Les microarchitectures Haswell et Broadwell=== Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. {|class="wikitable" |+ Microarchitecture Haswell et Broadwell |- ! !! 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/mémoire | ALU entière || ALU entière || ALU entière || ALU entière || LOAD/STORE || LOAD/STORE |- | ''Barrel shifter'' || Manipulation de bits || ''Barrel shifter'' || Manipulation de bits || || |- | || Multiplieur entier || ALU branchements || || || |- ! rowspan="4" | µops flottantes et SIMD | SIMD bit à bit (256 bits) || SIMD bit à bit (256 bits) || SIMD bit à bit (256 bits) || || || |- | || ALU SIMD entière (256 bits) || ALU SIMD entière (256 bits) || || || |- | FMUL + FMA (256 bits) || FMUL + FMA (256 bits) || || || || |- | FDIV || FADD || || || || |} ===La microarchitecture Skylake : la fin de la station de réservation centralisé=== L'architecture '''Skylake''' a eu une longévité hors du commun, avec près de 6 ans d’utilisation ininterrompue. Elle a été la concurrentes des microarchitectures Zen 1, Zen 2 et Zen 3 d'AMD. La microarchitecture ''Skylake'' est une microarchitecture capable de décoder/renommer 4 instructions par cycle, elle peut émettre 6 µops par cycle grâce au cache de µops. Elle a été déclinée en plusieurs microarchitectures légèrement différentes les unes des autres, nommés '''''Kaby Lake''''', '''''Coffee Lake''''', '''''Comet Lake'''''. Un changement extrêmement important concerne la station de réservation centralisé, qui ne l'est plus sur Skylake. Elle est scindée en deux stations de réservations : une pour les µops mémoire, et une autre pour le reste. Mien de rien, c'est à ce moment que la tendance "Intel = station de réservation centralisé " a commencée à s'adoucir. Intel est longtemps resté sur cette organisation avec un '''''Math Scheduler''''' d'un côté, et un '''''MEM scheduler''''' de l'autre. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===La microarchitecture ''Sunny Cove'' : la station de réservation mémoire est scindée=== Après Skylake, les microarchitecture d'Intel ont changé de dénomination. Elles sont maintenant nommées non pas avec des noms de lacs, mais ont un nom qui finit par Cove : ''Sunny Cove'', ''Golden Cove'', etc. La seule exception à cette règle est la microarchitecture '''''Palm Cove''''' était un ''die shrink'' de Skylake, ce qui fait qu'elle est à part de celles qui vont suivre, qui sont vraiment des microarchitectures différentes. La microarchitecture qui suit immédiatement skylake était la microarchitecture '''''Sunny Cove'''''. La microarchitecture''Sunny Cove'' suit la tendance entamée avec Skylake de casser la station de réservation principale en plusieurs stations de réservations plus petites. Cette fois-ci c'est la station de réservation pour les µops mémoire qui est scindée en deux une pour les lectures et une pour les écritures. La microarchitecture ''Sunny Cove'' passe de quadruple émission à la pentuple émission. Précisément, l'unité de renommage de registres est maintenant capable de renommer 5 µops par cycle. De plus, l'unité de renommage devient plus performante. Ses capacités de ''MOV elimination'' ont été améliorées, et elle devient capable de reconnaitre certains idiomes, comme celui qui met à zéro un registre en le XORant avec lui-même. ''Sunny Cove'' était au départ prévue pour les PC ''desktop'', mais sa production a eu de nombreux problèmes. Elle était censée être produite avec une technologie de gravure 10nm d'Intel lui-même, qui a cependant eu de nombreux problèmes de production. Au final, ''Sunny Cove'' et ses variantes n'ont existées que dans des CPU mobile, basse performance et basse consommation. Elle a été déclinée en deux versions : '''''Willow Cove''''' a un cache retravaillé, alors que '''''Cypress Cove''''' est l'équivalent de ''Sunny Cove'' mais repassé à une technologie de gravure de 14nm. Les deux microarchitecture ont été utilisées sur les plateformes dites '''''Ice Lake''''' et '''''Tiger Lake'''''. ===La microarchitecture ''Golden Cove'' : un ''Fetch'' doublé=== La microarchitecture '''Golden Cove''' a suivi ''Sunny Cove''. Les microarchitectures suivantes, ''Redwood Cove'' et ''Raptor Cove'', sont des modifications mineures de ''Golden Cove''. ''Golden Cove'' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Mais cela change avec ''Golden Cove''. Golden Cove double la taille du chargement qui passe à 32 octets. Le processeur peut décoder 6 instructions par cycle, et en émettre 6 à destination des ''schedulers''s. Il s'agit donc de processeurs superscalaires très larges, ce qui est la norme de nos jours et le restera pendant longtemps. ''Golden Cove'' conserve les trois ''schedulers'' séparées de ''Sunny Cove'', savoir : pour les instructions entières/flottantes, pour les lectures, pour les écritures. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] Niveau unités de calcul, peu de changements avec ''Sunny Cove'', si ce n'est une ALU entière en plus. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || |- | Branchements || Diviseur || || Branchements || |} ===La microarchitecture ''Lion Cove'' : la scission du ''math scheduler''=== La microarchitecture '''''Lion Cove''''' poursuit la tendance à scinder la station de réservation. La station de réservation pour les opérations entières/flottantes est maintenant scindée en deux : une pour les µops entières, une autre pour les µops SIMD/flottantes. Une autre modification, très liée à la précédente, est que le renommage de registre est lui aussi scindé. Les registres SIMD/flottants sont maintenant renommé indépendamment des registres généraux/entiers. Les raisons sont certainement que cela simplifie grandement la conception du processeur, en séparant les pipelines entiers et SIMD, surtout que les opérations entières et SIMD sont forts différentes : instructions 2-opérandes d'un côté, 2 à 3-opérandes pour le SIMD ; présence de techniques de ''masking'' pour le SIMD, mais pas pour les opérations entières, etc. Pour le reste, le processeur de 6 à 8 voies d'émission, et l'unité de chargement peut maintenant charger 48 octets, au lieu de 32 auparavant. Des ALU entières et SIMD ont été rajoutés. L'unité d'émission pour les opérations entières dispose de 6 ports d'émission, alors que celle pour les opérations SIMD n'en a que 4. {|class="wikitable" |+ Microarchitecture Haswell et Broadwell |- ! !! 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/mémoire | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || ALU entière |- | Branchements || ''Barrel shifter'' || Branchements || ''Barrel shifter'' || Branchements || ''Barrel shifter'' |- | || Multiplieur entier || || Multiplieur entier || || Multiplieur entier |} ==Les processeurs x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque ininterrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les micro-architectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise un ''scheduler'' centralisé. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par un ''scheduler'' centralisé. Les 4 micro-opérations renommées sont écrites dans le ''scheduler'' par groupe de 4, NOP de ''padding'' inclus. le ''scheduler'' centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de le ''scheduler'' est simple. le ''scheduler'' centralisé permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les micro-architectures K7, K8 et K10 d'AMD==== Les micro-architectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des ''schedulers''s, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une LSQ, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La LSQ est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la LSQ proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a un ''scheduler'' séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. le ''scheduler'' flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La micro-architecture K7 avait deux ''schedulers'' : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. le ''scheduler'' entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la LSQ mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la LSQ ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la LSQ. Elles s'exécutent alors l'une après l'autre. le ''scheduler'' entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans le ''scheduler'' entière. Et cela se marie bien avec un ''scheduler'' unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières au même ''scheduler'' pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la LSQ, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky micro-architecture]] La micro-architecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les ''schedulers''s/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===La micro-architecture Bulldozer et ses dérivés=== Viennent ensuite les '''micro-architectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Un détail très important de Bulldozer est que de nombreux circuits sont partagés entre deux cœurs, à un point qui mérite de nombreuses explications. Les processeurs Bulldozer peuvent exécuter deux programmes "en même temps", sur deux cœurs partiellement fusionnés. Cette technique de partage d'unités de calcul entre coeurs s'appelle le '''cluster multithreading''', ou encore les '''architectures à cœurs conjoints''' (''Conjoined Core Architectures''). Elle a aussi été utilisée sur le processeur Cortex A510, qui partageaient leur FPU entre deux coeurs. Les deux cœurs sont séparés pour ce qui est du chemin de données, du ''back-end''. Ils disposent de deux bancs de registres généraux et les ALU entières sont dupliquées. Toute la logique d'exécution dans le désordre est aussi dupliquée, avec deux ROBs, des stations de réservations séparées, etc. L'unité mémoire est aussi dupliquée, chaque cœur dispose de ses propres ''load queue'' et ''store queue'', ainsi de que son propre cache L1 de données. En clair, les chemins de données sont totalement séparés, sauf sur deux points : la FPU et les unités SIMD. La FPU est partagée entre deux coeurs, les unités SIMD sont aussi dans ce cas. Le partage de la FPU permet d'éviter de dupliquer trop de circuits et donc d'économiser des transistors. Le problème est que ce partage est source de dépendances structurelles, ce qui peut entraîner des pertes de performances. Par contre, le ''front-end'' est presque totalement partagé entre deux cœurs. Il n'y a qu'un seul cache d'instruction pour les deux cœurs, la TLB du cache d'instruction est unique, l'unité de prédiction de branchement est partagée entre deux cœurs, les décodeurs d'instruction aussi sont partagés, même la file de µops ! Les seules unités qui sont dupliquées sont la file d'instruction et l'unité de renommage, présentes en deux exemplaires. Concrètement, les deux programmes ont accès au cache à tour de rôle. Un programme a accès au cache à un instant t, mais le cycle suivant pourra être alloué à l'autre programme. Une unité décide de quel programme peut charger ses données, idem pour le décodage. [[File:AMD Bulldozer microarchitecture.png|centre|vignette|upright=3|Microarchitecture Bulldozer d'AMD.]] Au-delà de ce partage entre cœurs, Bulldozer a eu un changement assez important : les stations de réservation sont passées de stations de réservation très décentralisés à une station de réservation unique (pour les µops entières/mémoire), d'environ 40 entrées. L’implémentation de la station de réservation a aussi changée. Car 40 entrées, c'est beaucoup pour l'époque. Elle subit quelques simplifications pour la rendre plus rapide et plus économe en transistors. Les autres processeurs de l'époque utilisaient une station de réservation qui détectait à chaque instant quelle était l'instruction la plus ancienne, qui soit prête pour l’exécution. Mais Bulldozer n'utilisait pas cette technique. A la place, il mémorisait la place de l'instruction la plus ancienne gra^ce à une ''ancestry table''. Et l'instruction la plus ancienne est privilégiée sur toute les autres. Mais si elle n'est pas prête à s'exécuter, la station de réservation choisit une instruction suivant sa position dans la station de réservation (qui reste une mémoire RAM/associative, ne l'oublions pas). Bulldozer utilise aussi les optimisations du contournement et du banc de registre, vues dans le chapitre sur les CPU superscalaires. Le banc de registres généraux est dupliqué, afin de garder des bancs de registres avec peu de ports de lecture/écriture. Chaque banc de registre généraux a 4 ports de lecture et deux d'écriture, ce qui permet d'alimenter 2 ALU entières. En tout, cela permet d'alimenter 4 ALU entières, avec 8 ports de lecture et 4 d'écriture. Le tout alimente 2 ALU entières, et 2 AGUs/unités mémoire. Vu que la FPU est partagée entre deux programmes, sa station de réservation a une taille doublée comparé à la normale. Jugez plutôt : 60 entrées pour la station de réservation, là où les microarchitectures concurrentes d'Intel (''Sandy bridge'') utilisait une station de réservation de 54 entrées pour toutes les instructions entières/flottante/mémoire ! Idem pour le banc de registres flottants, qui n'a pas moins de 160 registres flottants ! Pour ce qui est des instructions SIMD, Bulldozer supporte l'extension AVX, qui fait des calculs du des registres de 256 bits, découpés en entiers/flottants de 64 bits, 32 bits, 16 bits, 8 bits, etc. L'unité SIMD ne gére cependant que des calculs sur 128 bits. Les instructions AVX sont donc découpées en deux µops, chacune traitant la moitié d'un registre SIMD. La performance n'est donc pas améliorée par rapport au SSE. Par contre, les unités SIMD gérent nativement l'opération FMA (une multiplication suivie d'une addition). {|class="wikitable" |- |+ Bulldozer |- ! Station de réservation 1 !! Station de réservation 2 !! Station de réservation 3 !! Station de réservation 4 |- | FMA - 128 bits || FMA - 128 bits || ALU - 128 bits || ALU - 128 bits |- | FMA opérandes entiers - 128 bits || Suffle et permutations || || FSTORE (x87) |} La révision Steamroller sépara le ''front-end'' en deux voies distinctes, une par ''thread''. Concrètement, le processeur se contente maintenant de partager la FPU et les unités SIMD entre deux coeurs, pas plus. Pour cela, elle ajouta un second décodeur d'instruction, une seconde file de micro-opération et une seconde unité de renommage de registres, afin d'améliorer les performances. Niveaux optimisations mineures, les stations de réservation ont été augmentées, elles peuvent mémoriser plus de micro-opérations, idem pour les bancs de registre et les files de lecture/écriture. Un cache de micro-opérations a été ajouté, de même que des optimisations quant au renommage de registre. Des ALU ont aussi été ajoutées, des FPU retirées. [[File:AMD excavator microarchitecture.png|centre|vignette|upright=3|Microarchitecture Excavator d'AMD.]] ===Les micro-architectures ZEN d'AMD=== Les micro-architectures suivantes sont les '''architectures ZEN 1/2/3/4'''. Elles se ressemblent beaucoup, le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La micro-architecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des ''schedulers'', notées ''scheduler'' dans le schéma. Les µops flottantes utilisent une station de réservation unique, alors qu'il y a une station de réservation par ALU entière. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Micro-architecture Zen 1 d'AMD.]] La micro-architecture Zen 5 se distingue fortement des 4 microarchitectures précédentes. Premièrement, elle passe à une station de réservation centralisé pour les µops entières, une seconde pour els µops mémoire, et une troisième pour les µops SIMD/flottantes. Le retour à une station de réservation aussi peu décentralisé n'avait pas été vu depuis la microarchitecture Bulldozer. Une autre différence est qu'elle utilise un décodeur ''clustered'', tel que vu dans le chapitre sur les processeurs superscalaires, dans la section "Le décodage superscalaire large". ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s’aperçoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===L'exécution dans le désordre superscalaire du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Précisément, il avait deux tables d'alias : une correspondant à l'état spéculatif du CPU, l'autre correspondant à l'état non-spéculatif. La première est utilisée pour renommer les µops, la seconde sert de sauvegarde en cas de mauvaise prédiction de branchement. Quand une cas de mauvaise prédiction de branchement, le pipeline est remis à zéro et l'unité re nommage doit être réinitialisée de manière à reprendre à la bonne instruction. Pour cela, la table d'alias spéculative était réinitiailisée avec le contenu de la seconde. [[File:MIT 6.823 Computer System Architecture, Fall 2005 - Pentium III and Netburst - 4332527833.jpg|centre|vignette|upright=2.5|Tables d'alias du Pentiçum 4.]] Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de ''schedulers'' ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des ''schedulers'' : centralisés pour Intel, décentralisés pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] [[File:MIT 6.823 Computer System Architecture, Fall 2005 - P-4 Execution Ports - 4332668231.jpg|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:MIT 6.823 Computer System Architecture, Fall 2005 - P-4 Microarchitecture - 4472656667.jpg|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. ==Les microarchitectures x86 de Cyrix et Via== Les processeurs x86 grand public sont actuellement conçus et commercialisé par deux acteurs : Intel et AMD. Il faut dire que le jeu d'instruction x86 demande une licence pour être utilisée, qu'Intel et AMD refusent de dsitribuer à leurs concurrents, sauf en de très rares occasions. Cependant, il y a eu dans le passé un troisième acteur, qui a reussit à acquérir les droits de la licence x86 : Cyrix, qui a ensuite été racheté par National Semiconductor, lui-même acquis par Via. ===Les architectures compatibles 386 et 486 de Cyrix=== Cyrix a produits plusieurs CPUs et même des co-processeurs x87, avant d'être racheté. Sa carrière commence avec le Cyrix FasMath, un co-processeur arithmétique pour calculs flottants, compatible avec le x87 d'Intel. Il a ensuite produits plusieurs clones du processeur 386 d'Intel, nommés 486SLC et 486DLC. Une dénomination assez trompeuse, vous en conviendrez, mais qui n'est heureusement pas passée inaperçue et leur a valu de nombreuses critiques. Néanmoins, leurs processeurs étaient plus performants que le 386 de base, ce qui fait qu'ils étaient des upgrades corrects pour les PC avec un 386 (les cartes mères compatibles avec le i386 n’étaient pas compatibles avec le i486). Le 486SRX2 et le 486DRX2, sortis plus tard, étaient des versions avec une fréquence encore plus élevée, ce qui augmentait leurs performances. Cyrix récidiva avec le 5x86, un processeur offrant des performances s'approchant du Pentium, mais pour la plateforme 486. Le processeur supportait seulement les instructions du 486, pas les instructions ajoutées sur le Pentium. Son bus était similaire à celui du 486. Mais sa microarchitecture se rapprochait du Pentium, bien que n'étant pas aussi complexe. Il y avait une file d'instruction capable de mettre en attente 3 instructions, un cache unifié, une unité de prédiction de branchement. Malheureusement, la prédiction de branchements avait été désactivée en raisons de bugs matériels qui faisaient crasher le processeur. Niveau ALU, mais il y avait une FPU, une ALU entière et une unité mémoire. Mais il n'y avait pas de double émission. [[File:Cy5x86 arch (cropped).png|centre|vignette|upright=2.5|Cy5x86 arch (cropped).]] ===Le Cyrix 6x86 : une architecture superscalaire avancée=== Le '''Cyrix 6x86''' est là où la superscalarité apparait dans la gamme Cyrix. Le processeur était non pas une upgrade pour des cartes mères existantes, mais un concurrent direct du Pentium. C'était un processeur à double émission, avec exécution dans le désordre et renommage de registre. L'organisation de ses caches est assez originale. Il avait un cache principal de 16 kibioctets, qui mémorisait à la fois données et instructions, couplé à un cache d'instruction de 256 octets. En termes modernes, on dirait qu'il disposait d'un cache L2, couplé à un cache L1 d'instructions, mais sans cache L1 pour les données. Le cache unifié/L2 était un cache double port. Le cache d'instruction était totalement associatif. [[File:Cy6x86 arch.png|centre|vignette|upright=2.5|Cy6x86 microarchitecture.]] Le processeur supportait une double émission bien plus complète que celle du Pentium. Il disposait de deux pipelines entiers, nommés X et Y. Le pipeline X était le seul à pouvoir exécuter branchements et opérations flottantes, c'était le seul connecté à la FPU. Par contre, il était possible d'émettre un branchement ou une opération flottante dans le pipeline X, tout en émettant une seconde instruction dans le pipeline Y. Ce que le Pentium ne pouvait pas faire. Certaines instructions ne pouvaient cependant pas être appariées avec une autre : les multiplications/divisions, les ''string instruction'', les ''far jumps'', les accès aux entrées-sorties, et des instructions systèmes. La FPU pouvait exécuter des instructions en parallèle de l'ALU entière. Elle avait une file de µops de 4 µops flottantes. Elle avait aussi sa propre ''Store queue'', spécifiquement dédiée aux écritures flottantes, séparée de la ''Store Queue'' pour les écritures "normales", celles des registres généraux. Cette séparation est possible du fit que les registres flottants et généraux sont séparés sur le jeu d'instruction x86. [[File:Cyrix 6x86 arch.svg|centre|vignette|upright=2.5|Cy6x86 microarchitecture.]] Le Cyrix 6x86 avait de nombreux avantages sur le Pentium. Il intégrait notamment du renommage de registres pour les registres entiers, ainsi que le contournement, ce que ne faisait pas le Pentium. Et il pouvait faire de la double émission entière-flottante, alors que le Pentium ne pouvait pas émettre de seconde µop en parallèle avec une opération flottante. Tout cela le rendait bien plus performant que le Pentium, sur le papier. Sauf que sa FPU était peu performante, proche de celle du 5x86. Les concepteurs du processeur avaient mis le paquet sur les opérations entières, en délaissant les opérations flottantes. Les applications développées pour le Pentium, qui répartissaient au mieux leurs calculs entre FPU et ALU entière, marchaient donc moins bien sur le 6x86. Le successeur du 6x86, le '''Cyrix Jalapeno''', était un concurrent du Pentium 2/3. Son pipeline passait de 7 à 11 étages, ce qui permettait des augmentation de fréquence importante. Son cache était quadruplé en taille et passait à 8 voies. Et pour ce qui de la superscalarité, il était maintenant capable d'émettre deux µops flottantes par cycle : une partait dans l'additionneur flottant, l'autre dans un multiplieur flottant. ===Les processeurs de marque Via=== Après deux rachats, la licence x86 de Cyrix est passée dans les main de Via. Via avait racheté Cyrix, mais aussi Centaur, un autre concepteur de processeur. Via produisit plusieurs processeurs x86, basés sur des design provenant non pas de Cyrix, mais de Centaur, pour leur microarchitecture. Le premier processeur de cette lignée, le mal-nommé Cyrix III, a d'ailleurs été renommé en C3, parce qu'il n'avait aucun lien avec les microarchitectures de Cyrix et n'utilisait pas leurs technologies. Les '''processeurs Via C3''' et '''Via C7''' étaient des processeurs 32 bits. Ils étaient prévus pour des ordinateurs portables et ciblainet une faible consommation d'énergie, plutôt que des hautes performances. Ils n'avaient pas d'exécution dans le désordre, mais utilisaient une superscalarité limitée à la double émission. Le C3 regroupe en réalité deux microarchitecture semblables. Les modèles Samuel 2 et Ezra utilisaient la première version de la microarchitecture, alors que les processeurs Nehemiah avaient une version améliorée de celle-ci. Le pipeline passait de 12 à 16 étages, sa FPU est améliorée, les extensions SIMD sont revues (passage au SSE, abandon du 3DNow!). Les processeurs C7 utilisent cette même microarchitecture, bien qu'améliorée sur quelques détails. [[File:VIA Nano Architecture Blockdiagram.jpg|vignette|upright=1.5|VIA Nano Architecture.]] Par la suite, Via sortit le '''Via Nano''' en 2008 . C'était le premier CPU 64 bits de la marque. Il avait tout d'un design moderne, avec exécution dans le désordre, prédiction de branchement complexe, hiérarchie de cache classique, préchargement, etc. Il implémentait la superscalarité en triple émission. Pour cela, son ''front-end'' chargeait 16 octets par cycle, qui étaient accumulés dans une file d'instruction. Trois décodeurs lisaient les instructions dans cette file, puis envoyaient trois µops dans une file de µops de taille inconnue. Le renommage de registre pouvait lui aussi renommer trois µops par cycle. Niveaux unités, il avait deux ALU entières, une unité LOAD, une unité STORE, deux unités SIMD (une pour les additions SIMD, une pour les multiplications SIMD). Chaque unité avait sa propre file de µops, qui faisait entre 8 et 12 entrées. Les unités LOAD et STORE avaient chacune leur propre file de µops, il n'y avait pas de file de µops partagée entre les deux. L'unité LOAD alimentait une ''load queue'' de 16 lectures, alors que l'unité STORE alimentait une ''store queue'' de 16 écritures. Les unités de calcul sont décrites dans le tableau ci-dessous : {|class="wikitable" |- ! !! ALU 1 !! ALU 2 !! FPU/SIMD 1 !! FPU/SIMD 2 !! LOAD !! STORE |- ! File de µops | 12 entrées || 11 entrées || 8 entrées || 24 entrées || 8 entrées || 8 à 13 entrées, selon les sources |- ! rowspan="5" | Opérations | Opérations entières de base || Opérations entières de base || || || LOAD || STORE |- | Décalages et rotations || Multiplication entière 32 bits || Multiplication entière 64 bits || || || |- | CMOV || CMOV || Division entière 64 bits || Division entière 64 bits || || |- | ''Add With Carry'' || Branchements || Division flottante || || || |- | || || SIMD opérations entières + FMUL 128 bits || SIMD opérations entières + FADD 128 bits || || |} Les processeurs qui ont suivis sont des processeurs commercialisés uniquement en Chine, à destination du marché chinois et subventionnés (si ce n'est plus), par l'administration chinoise.Le peu d'informations qui sont disponibles dessus sont disponibles sur le substack ''Chips and cheese'', qui a notamment quelques articles sur les processeurs Longsoon. Pour une histoire complète et une review en profondeur des architectures de Via, je conseille les articles du blog ''Chips and cheese'' : * [https://chipsandcheese.com/p/the-weird-and-wacky-world-of-via-the-3rd-player-in-the-modern-x86-market The Weird and Wacky World of VIA, the 3rd player in the “Modern” x86 market] * [https://chipsandcheese.com/p/the-weird-and-wacky-world-of-via-part-2-zhaoxins-not-quite-electric-boogaloo The Weird and Wacky World of VIA Part 2: Zhaoxin’s not quite Electric Boogaloo] * [https://chipsandcheese.com/p/zhaoxin-part-3-a-sort-of-anti-climax Zhaoxin Part 3: A Sort of Anti-Climax] * [https://chipsandcheese.com/p/via-part-4-a-deep-dive-into-centaurs-last-cpu-core-cns VIA Part 4 – A Deep Dive into Centaur’s Last CPU Core: CNS] * [https://chipsandcheese.com/p/examining-centaur-chas-die-and-implementation-goals Examining Centaur CHA’s Die and Implementation Goals] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les premiers processeurs Intel | prevText=Les premiers processeurs Intel | next=Les microarchitectures pour le x86 : basse consommation | nextText=Les microarchitectures pour le x86 : basse consommation }} </noinclude> lzbnkym22kr4oaoa5n1e7lsw0qh03ou Discussion Wikilivres:Le Bistro/2026 5 83406 775129 774242 2026-10-13T07:39:53Z MediaWiki message delivery 36013 /* Tech News: 2026-42 */ nouvelle section 775129 wikitext text/x-wiki == Actualités techniques n° 2026-03 == <section begin="technews-2026-W03"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/03|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * La Fondation Wikimedia a publié des questions directrices pour son plan annuel de juillet 2026 à juin 2027 sur les plateformes [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] et ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. Celles-ci portent sur les tendances mondiales, une expérimentation plus rapide et plus constructive, un meilleur accompagnement des nouveaux contributeurs, le renforcement du rôle des éditeurs et des utilisateurs avancés, l'amélioration de la collaboration entre les projets, ainsi que le développement et la fidélisation du lectorat. Des commentaires et suggestions sont les bienvenus sur la [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|page de discussion]]. '''Actualités pour la contribution''' * Dans le cadre des travaux en cours de l'équipe technique communautaire sur le projet [[m:Special:MyLanguage/Community Wishlist/W372|Listes de surveillance multiples]], l'affichage de [[Special:EditWatchlist|Modifier la liste de surveillance]] sera mis à jour entant que qu'une première étape vers la prise en charge de plusieurs listes de surveillance. De plus, la pagination de [[Special:Search|Recherche]] sera également mise à jour, dans le cadre du travail sur le souhait [[m:Special:MyLanguage/Community Wishlist/W186|Refonte de la pagination / navigation des pages]]. [https://phabricator.wikimedia.org/T411596] * [[m:Special:GlobalWatchlist|La Liste de Surveillance Globale]] est une [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] de MediaWiki qui vous permet de voir vos listes de surveillance provenant de différents wikis sur la même page. Il a récemment été mis à jour pour ressembler davantage à la [[Special:Watchlist|Liste de surveillance]] régulière, par exemple en le préparant pour les comptes temporaires dans le masquage IP (y compris le réacheminement des liens des utilisateurs vers les pages de contributions), en mettant les titres de page en gras et en ouvrant les liens dans les résumés d'édition et les balises dans de nouveaux onglets du navigateur. [https://phabricator.wikimedia.org/T398361][https://phabricator.wikimedia.org/T298919][https://phabricator.wikimedia.org/T273526][https://phabricator.wikimedia.org/T286309] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:28|la tâche soumise|les {{formatnum:28}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:28||s}} la semaine dernière]]. Par exemple, le problème selon lequel les blocs globaux ne disposaient pas de l'option permettant de désactiver l'envoi d'e-mails a maintenant été résolu et sera disponible à l'utilisation à partir de la semaine du 13 janvier. [https://phabricator.wikimedia.org/T401293] '''Actualités pour la contribution technique''' * L'[[mw:Special:MyLanguage/VisualEditor/Citation tool|outil de citation VisualEditor]] et les [[mw:Special:MyLanguage/Help:Reference Previews|Aperçus de référence]] prennent désormais en charge "carte" comme type de référence. [https://phabricator.wikimedia.org/T411083] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.10|MediaWiki]]/[[mw:MediaWiki 1.46/wmf.11|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/03|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W03"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 12 janvier 2026 à 20:33 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29907192 --> == Thank You for Last Year – Join Wiki Loves Ramadan 2026 == Dear Wikimedia communities, We hope you are doing well, and we wish you a happy New Year. ''Last year, we captured light. This year, we’ll capture legacy.'' In 2025, communities around the world shared the glow of Ramadan nights and the warmth of collective iftars. In 2026, ''Wiki Loves Ramadan'' is expanding, bringing more stories, more cultures, and deeper global connections across Wikimedia projects. We invite you to explore the ''Wiki Loves Ramadan 2026'' [[m:Special:MyLanguage/Wiki Loves Ramadan 2026|Meta page]] to learn how you can participate and [[m:Special:MyLanguage/Wiki Loves Ramadan 2026/Participating communities|sign up]] your community. 📷 ''Photo campaign on '' [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan 2026|Wikimedia Commons]] If you have questions about the project, please refer to the FAQs: * [[m:Special:MyLanguage/Wiki Loves Ramadan/FAQ/|Meta-Wiki]] * [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan/FAQ|Wikimedia Commons]] ''Early registration for updates is now open via the '''[[m:Special:RegisterForEvent/2710|Event page]]''''' ''Stay connected and receive updates:'' * [https://t.me/WikiLovesRamadan Telegram channel] * [https://lists.wikimedia.org/postorius/lists/wikilovesramadan.lists.wikimedia.org/ Mailing list] We look forward to collaborating with you and your community. '''The Wiki Loves Ramadan 2026 Organizing Team''' 16 janvier 2026 à 20:44 (CET) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29879549 --> == <span lang="en" dir="ltr">Tech News: 2026-04</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W04"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/04|Translations]] are available. '''Updates for editors''' * The tray shown on [[Special:Diff|Special:Diff]] in mobile view has been redesigned. It is now collapsed by default, and incorporates a link to undo the edit being viewed, making it easier for mobile editors and reviewers to take action while keeping the interface uncluttered. [https://phabricator.wikimedia.org/T402297] * [[m:Special:GlobalWatchlist|The Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continues to improve — it now automatically determines the text direction (ensuring correct display of sites with unusual domain names) and shows detailed descriptions for log actions. Later this week, a new permanent link for page creations and CSS classes for each entry element will be added. [https://phabricator.wikimedia.org/T412505][https://phabricator.wikimedia.org/T287929][https://phabricator.wikimedia.org/T262768][https://phabricator.wikimedia.org/T414135] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:32}} community-submitted {{PLURAL:32|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the previously observed issue in Vector 2022, where anchor link targets were obscured by the sticky header, has now been addressed. [https://phabricator.wikimedia.org/T406114] '''Updates for technical contributors''' * As mentioned in the [[m:Special:MyLanguage/Tech/News/2025/44|October 2025 deprecation announcement]], MediaWiki Interfaces team will begin sunsetting all transform endpoints containing a trailing slash from the MediaWiki REST API the week of January 26. Changes are expected to roll out to all wikis on or before January 30th. All API users currently calling them are encouraged to transition to the non-trailing slash versions. Both endpoint variations can be found, compared, and tested using the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox]. If you have questions or encounter any problems, please file a ticket in Phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * Interactive reference documentation for the [[mw:Special:MyLanguage/Wikimedia REST API|Wikimedia REST API]] has moved. Requests to API docs previously hosted through [[mw:Special:MyLanguage/RESTBase|RESTBase]] (e.g.: <code dir=ltr>https://en.wikipedia.org/api/rest_v1/</code>) are now redirected to the [[w:en:Special:RestSandbox|REST Sandbox]]. * The [[mw:Special:MyLanguage/Wikidata Platform|WMF Wikidata Platform team]] (WDP) has published its [[d:Special:MyLanguage/Wikidata:Wikidata Platform team/Newsletter|January 2026 newsletter]]. It includes updates on the legacy full-graph endpoint decommissioning, the User-Agent policy change, the monthly Blazegraph migration office hours, and efforts to reduce regressions caused by the legacy endpoint shutdown. As a reminder, you can [[m:Special:MyLanguage/Global message delivery/Targets/WDP team updates|subscribe to the WDP newsletter]]! * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.12|MediaWiki]] '''Meetings and events''' * The [[mw:Wikimedia Hackathon Northwestern Europe 2026|Wikimedia Hackathon Northwestern Europe 2026]] will take place on 13-14 March 2026 in Arnhem, the Netherlands. Applications opened mid-December and will close soon or when capacity is reached. It's a two-day, technically oriented hackathon bringing together Wikimedians from the region. Hope to see you there! '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/04|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W04"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 19 janvier 2026 à 21:29 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29943403 --> == Révision annuelle du code universel de conduite et des lignes directrices de l'application == <section begin="announcement-content" /> Nous vous informons que la période de relecture annuelle du Code de conduite universel et des règles d'applications est actuellement ouverte. Vous pouvez faire vos commentaires sur les modifications que vous souhaitez apporter jusqu'au 9 février 2026. C'est la première d'une série d'étapes nécessaires pour la révision annuelle. Vous trouverez [[m:Special:MyLanguage/Universal Code of Conduct/Annual review/2026|d'autres informations et les discussions auxquelles participer sur la page UCoC de Meta]]. Le [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee|Comité de coordination du code universel de conduite]] (U4C &mdash; Universal Code of Conduct Coordinating Committee) est un groupe global dont le rôle est de fournir une implémentation équitable et cohérente de l'UCoC. Cette relecture annuelle a été envisagée et mise en place par l'U4C. Pour plus d'informations et les responsabilités de l'U4C, veuillez lire la [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee/Charter|Charte de l'U4C]]. Veuillez partager ces informations avec les autres membres concernés de votre communauté. -- En coopération avec l'U4C, [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|discussion]])<section end="announcement-content" /> 19 janvier 2026 à 22:01 (CET) <!-- Message envoyé par User:Keegan (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=29905753 --> == Actualités techniques n° 2026-05 == <section begin="technews-2026-W05"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/05|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La Fondation Wikimedia invite à donner des commentaires sur [[m:Special:MyLanguage/Product and Technology Advisory Council/Year1 Reflections and Proposed Way Forward 2026 Update|l’avenir proposé]] du [[:m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif des produits et technologies]] jusqu’au 28 février. * Tous les utilisateurs disposant d'un compte enregistré peuvent désormais utiliser des clés d'accès pour la [[m:Special:MyLanguage/Help:Two-factor authentication|double authentification]] (2FA). Les clés d'accès sont un moyen simple de se connecter sans utiliser un second appareil. Elles vérifient l'identité de l'utilisateur à l'aide d'une empreinte digitale, d'une reconnaissance faciale ou d'un code PIN. Pour configurer une clé d'accès, configurez d'abord une méthode 2FA classique. Actuellement, pour se connecter avec une clé d'accès, les utilisateurs doivent également utiliser un mot de passe. Plus tard ce trimestre, la connexion sans mot de passe permettra aux utilisateurs de se connecter d'un simple clic avec une clé d'accès. Les utilisateurs disposant de droits avancés devront également avoir la 2FA activée. Cela fait partie du projet [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security|Sécurité du compte]]. * Les contributeurs non enregistrés sur des IP bloquées ou des plages d'IP bloquées peuvent désormais interagir sur le wiki pour faire appel d'un blocage en créant un compte temporaire afin de contester un blocage sur la page de discussion de l'utilisateur, sauf si l'option « empêcher cet utilisateur de modifier sa propre page de discussion » est activée. Cela résout le problème des utilisateurs déconnectés incapables d'utiliser le processus de déblocage par défaut via la page de discussion de l'utilisateur. [https://phabricator.wikimedia.org/T398673] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:20|la tâche soumise|les {{formatnum:20}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:20||s}} la semaine dernière]]. Par exemple, la description des méthodes d'authentification à deux facteurs (2FA) sur la page de gestion a été mise à jour. Il est désormais plus clair et plus facile pour les utilisateurs à comprendre et à utiliser. [https://phabricator.wikimedia.org/T332385] '''Actualités pour la contribution technique''' * Une nouvelle variable AbuseFilter, <code>account_type</code>, a été ajoutée pour fournir un moyen fiable de déterminer le type de compte créé dans les actions <code>createaccount</code> et <code>autocreateaccount</code>. Dans le cadre de ce changement, la variable <code>accountname</code> a été renommée en <code>account_name</code>, et <code>accountname</code> est désormais obsolète. Les gestionnaires de filtres doivent mettre à jour tous les filtres qui utilisent des vérifications de type de compte codées en dur ou la variable obsolète. [https://phabricator.wikimedia.org/T414049] * Les vignettes d'images demandées dans des tailles non standard, et en utilisant des méthodes non standard telles que les requêtes directes à <code dir=ltr><nowiki>upload.wikimedia.org/…</nowiki></code>, cesseront de fonctionner dans un proche avenir. Ce changement vise à prévenir les abus externes continus par des robots et des aspirateurs web. Certains utilisateurs ayant des CSS/JS personnalisés, les administrateurs d'interface qui peuvent corriger les gadgets et les thèmes locaux, ainsi que les auteurs d'outils, devront mettre à jour leur code pour utiliser des tailles de vignettes standard. [[phab:T414805|Des détails, des liens de recherche et des exemples de correction sont disponibles dans la tâche]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.13|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/05|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W05"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 26 janvier 2026 à 22:17 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29969530 --> == <span lang="en" dir="ltr">Tech News: 2026-06</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W06"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/06|Translations]] are available. '''Updates for editors''' * The "{{int:pageinfo-toolboxlink}}" feature, which gives validating information about a page ([{{fullurl:{{FULLPAGENAME}}|action=info}} example]), now automatically includes a table of contents. If there is a local [[{{ns:8}}:Pageinfo-header]] page created by individual users, it can now be removed. [https://phabricator.wikimedia.org/T363726] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, VisualEditor previously added bold or italic formatting inside link descriptions, making the wikicode complex. This has now been fixed. [https://phabricator.wikimedia.org/T409669] '''Updates for technical contributors''' * There was no XML dump on 20 January. Additionally, from now on, dumps will be generated once per month only. [https://phabricator.wikimedia.org/T414389] * The MediaWiki Interfaces team removed support for all transform endpoints containing a trailing slash from the [https://www.mediawiki.org/wiki/Special:MyLanguage/API:REST%20API MediaWiki REST API]. All API users currently calling those endpoints are encouraged to transition to the non-trailing slash versions. If you have questions or encounter any problems, please file a ticket in phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.14|MediaWiki]] '''Weekly highlight''' * Users are reminded that the Wikimedia Foundation has shared some guiding questions for the July 2026–June 2027 Annual Plan on [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] and ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. These focus on global trends, faster and healthier experimentation, better support for newcomers, strengthening editors and advanced users, improving collaboration across projects, and growing and retaining readership. Feedback and ideas are welcome on the [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|talk page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/06|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W06"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 février 2026 à 18:43 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30000986 --> == Actualités techniques n° 2026-07 == <section begin="technews-2026-W07"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/07|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] Les contributeurs connectés qui gèrent de grandes ou complexes listes de suivi peuvent désormais organiser et filtrer les pages surveillées de manière à améliorer leurs flux de travail grâce à la nouvelle fonctionnalité [[mw:Special:MyLanguage/Help:Watchlist labels|Étiquettes de liste de suivi]]. En ajoutant des étiquettes personnalisées (par exemple : pages que vous avez créées, pages surveillées pour vandalisme, ou pages de discussion), les utilisateurs peuvent identifier plus rapidement ce qui nécessite une attention, réduire la charge cognitive et répondre plus efficacement. Cela améliore l'utilisabilité de la liste de suivi, en particulier pour les éditeurs très actifs. * Une nouvelle fonctionnalité disponible sur [[Special:Contributions|Special:Contributions]] montre [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts|des comptes temporaires]] qui sont probablement utilisés par la même personne, et rend ainsi le patrouillage moins chronophage. En vérifiant les contributions d'un compte temporaire, les utilisateurs ayant accès aux adresses IP des comptes temporaires peuvent désormais avoir une vue des contributions des comptes temporaires associés. La fonctionnalité recherche toutes les adresses IP associées à un compte temporaire donné pendant la période de conservation des données et affiche toutes les contributions de tous les comptes temporaires ayant utilisé ces adresses IP. [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts#February 2026: Improvements to the patroller tooling|Plus...]] [https://phabricator.wikimedia.org/T415674] * Lorsque les éditeurs prévisualisent une modification de wikitexte, la boîte de rappel indiquant qu'ils ne voient qu'une prévisualisation (qui est affichée en haut) a désormais un fond gris/neutre au lieu d'un fond jaune/d'avertissement. Cela facilite la distinction entre les notes de prévisualisation et les avertissements réels (par exemple, les conflits de modification ou les cibles de redirection problématiques), qui seront désormais affichés dans des boîtes d'avertissement ou d'erreur séparées. [https://phabricator.wikimedia.org/T414742] * La [[m:Special:GlobalWatchlist|Liste de suivi globale]] vous permet de consulter vos listes de suivi provenant de plusieurs wikis sur une seule page. L' [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continue de s'améliorer — elle prend désormais en charge correctement plus d'un site Wikibase, par exemple à la fois [[d:|Wikidata]] et [[testwikidata:|testwikidata]]. De plus, des problèmes concernant la direction du texte ont été résolus pour les utilisateurs qui préfèrent Wikidata ou d'autres sites Wikibase dans des langues de droite à gauche (RTL). [https://phabricator.wikimedia.org/T415440][https://phabricator.wikimedia.org/T415458] * <span lang="en" dir="ltr" class="mw-content-ltr">The automatic "magic links" for ISBN, RFC, and PMID numbers have been [[mw:Special:MyLanguage/Help:Magic links|deprecated in wikitext since 2021]] due to inflexibility and difficulties with localization. Several wikis have successfully replaced RFC and PMID magic links with equivalent external links, but a template was often required to replace the functionality of the ISBN magic link. There is now a new [[mw:Special:MyLanguage/Help:Magic words#isbn|built-in parser function]] <code dir=ltr><nowiki>{{#isbn}}</nowiki></code> available to replace the basic functionality of the ISBN magic link. This makes it easier for wikis who wish to migrate off of the deprecated magic link functionality to do so.</span> [https://phabricator.wikimedia.org/T145604] * Deux nouveaux wikis ont été créés : ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q35401|Jju]] ([[w:kaj:|<code>w:kaj:</code>]]) [https://phabricator.wikimedia.org/T413283] ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q1186896|Nawat]] ([[w:ppl:|<code>w:ppl:</code>]]) [https://phabricator.wikimedia.org/T413273] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * Un nouveau groupe d'utilisateurs global a été créé : [[{{int:grouppage-local-bot}}|{{int:group-local-bot}}]]. Il sera utilisé en interne par le logiciel pour permettre aux robots communautaires de contourner les limites de débit appliquées aux [[w:en:Web_scraping|web scrapers]] abusifs. Les comptes approuvés en tant que robots sur au moins un wiki Wikimedia seront automatiquement ajoutés à ce groupe. Cela ne changera pas les autorisations dont dispose le robot. [https://phabricator.wikimedia.org/T415588] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.15|MediaWiki]] '''Rencontres et évènements''' * La [[mw:Special:MyLanguage/MediaWiki Users and Developers Conference Spring 2026|Conférence des utilisateurs et des développeurs de MediaWiki, Printemps 2026]] se tiendra du 25 au 27 mars à Salt Lake City, États-Unis. Cet événement est organisé par et pour la communauté MediaWiki de tiers. Vous pouvez proposer des sessions et vous inscrire pour y assister. [https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/AZBWVI46SDEB65PGR5J6E4TYOQQEZXM7/] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/07|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W07"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 10 février 2026 à 00:30 (CET) <!-- Message envoyé par User:Quiddity (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30026671 --> == Actualités techniques n° 2026-08 == <section begin="technews-2026-W08"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/08|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * <span class="mw-translate-fuzzy">L'[[mw:Special:MyLanguage/Wikimedia Site Reliability Engineering|équipe SRE]] va procéder au nettoyage d'[[m:Special:MyLanguage/Etherpad|Etherpad]], l'éditeur web open source de documents collaboratifs en temps réel. Tous les blocs-notes seront définitivement supprimés après le 30 avril 2026 – si des projets de migration sont encore en cours à cette date, l'équipe pourra réexaminer la date au cas par cas. Veuillez effectuer des sauvegardes locales de tout contenu que vous souhaitez conserver, car les données supprimées ne pourront pas être récupérées. Ce nettoyage permet de réduire la taille de la base de données et l'empreinte de l'infrastructure. Etherpad continuera de prendre en charge la collaboration en temps réel, mais le stockage à long terme n'est plus assuré. D'autres nettoyages pourront avoir lieu ultérieurement sans préavis.</span> [https://phabricator.wikimedia.org/T415237] '''Actualités pour la contribution''' * L'équipe de Recherche d'Informations lancera une [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|expérimentation sur l'application mobile Android]], afin de tester des fonctionnalités de recherche hybrides capables de gérer à la fois les requêtes sémantiques et par mots-clés. L'amélioration de la recherche sur la plateforme permettra aux lecteurs de trouver plus facilement ce qu'ils cherchent, directement sur Wikipédia. L'expérimentation sera d'abord lancée sur Wikipédia en grec fin février, puis sur les versions anglaise, française et portugaise en mars. [https://diff.wikimedia.org/2026/01/08/semantic-search-making-it-easier-to-find-the-information-readers-want/ En savoir plus] sur le blog ''Diff''. [https://www.mediawiki.org/wiki/Readers/Information_Retrieval] * L'équipe « Croissance des lecteurs » mènera [[mw:Special:MyLanguage/Readers/Reader Growth/WE3.10.2 Mobile Table of Contents|une expérience]] auprès des utilisateurs de la version mobile du site web qui ajoute une table des matières et développe automatiquement toutes les sections des articles, afin de mieux comprendre les problèmes de navigation qu'ils rencontrent. Le test sera disponible sur les versions arabe, chinoise, anglaise, française, indonésienne et vietnamienne de Wikipedia. * Auparavant, les notifications ([[{{ns:8}}:Sitenotice]] et [[{{ns:8}}:Anonnotice]]) du site ne s'affichaient que sur la version ordinateur. Maintenant, elles s'afficheront désormais sur toutes les plateformes. Les utilisateurs mobiles verront ces notifications. Les administrateurs du site doivent être prêts à tester et à corriger les notifications sur les appareils mobiles afin d'éviter toute interférence avec les articles. Pour désactiver ces notifications, les administrateurs d'interface peuvent ajouter <code dir="ltr">#siteNotice { display: none; }</code> à [[{{ns:8}}:Minerva.css]]. [https://phabricator.wikimedia.org/T138572][https://phabricator.wikimedia.org/T416644] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:19|la tâche soumise|les {{formatnum:19}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:19||s}} la semaine dernière]]. Par exemple, un problème concernant la section ''[[Special:RecentChanges|Spécial:Modifications récentes]]'' a été résolu. Auparavant, cliquer sur « Masquer » dans les filtres actifs entraînait la disparition du bouton « Afficher les nouvelles modifications depuis… », alors qu'il aurait dû rester visible. Ce bouton fonctionne désormais correctement. [https://phabricator.wikimedia.org/T406339] '''Actualités pour la contribution technique''' * Une nouvelle documentation est désormais disponible pour aider les rédacteurs à déboguer les fonctionnalités de recherche interne. Elle facilite le dépannage lorsque des pages n'apparaissent pas dans les résultats, lorsque le classement semble inattendu et lorsqu'il est nécessaire d'inspecter le contenu indexé, ce qui permet de mieux comprendre et d'analyser le comportement de la recherche. [[mw:Help:CirrusSearch/Debug|En savoir plus]]. [https://phabricator.wikimedia.org/T411169] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/08|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W08"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 février 2026 à 20:17 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30086330 --> == <span lang="en" dir="ltr">Tech News: 2026-09</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W09"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/09|Translations]] are available. '''Weekly highlight''' * [[mw:Special:MyLanguage/Edit check/Reference Check|Reference Check]] has been deployed to English Wikipedia, completing its rollout across all Wikipedias. The feature prompts newcomers to add a citation before publishing new content, helping reduce common citation-related reverts and improve verifiability. In A/B testing, the impact was substantial: newcomers shown Reference Check were approximately 2.2 times more likely to include a reference on desktop and about 17.5 times more likely on mobile web. [https://analytics.wikimedia.org/published/reports/editing/reference_check_ab_test_report_final_2025.html] '''Updates for editors''' * The [[mw:Special:MyLanguage/Extension:InterwikiSorting|InterwikiSorting extension]], which allowed for the [[m:Special:MyLanguage/Interwiki sorting order|sorting of interwiki links]], has been undeployed from Wikipedia. As a result, editors who had enabled interwiki link sorting in non-compact mode (full list format) will now see links reordered. The links moving forward will be listed in the alphabetical order of language code. [https://phabricator.wikimedia.org/T253764] * Later this week, people who are editing a page-section using the mobile visual editor, will notice a new "Edit full page" button. When tapped, you will be able to edit the entire article. This helps when the change you want to make is outside the section you initially opened. [https://phabricator.wikimedia.org/T387175][https://phabricator.wikimedia.org/T409112] * [[mw:Special:MyLanguage/Readers/Reader Experience|The Reader Experience team]] is inviting editors to assess whether dark mode should still be considered "beta" on their wiki, based on their experience of how well it functions on desktop and mobile. If the feature is deemed mature, editors can update the interface messages in <code dir=ltr>MediaWiki:skin-theme-description</code> and <code dir=ltr>MediaWiki:Vector-night-mode-beta-tag</code> to indicate that dark mode is ready and no longer considered beta. * The improved [[mw:Wikimedia_Apps/Team/iOS/Activity_Tab|Activity tab]] which displays user-insights is now available to all users of the Wikipedia iOS app (version 7.9.0 and later). Following earlier A/B testing that showed higher account creation among users with access to the feature, it has been rolled out to 100% of users along with some updates. The Activity tab now shows your edited articles in the timeline, offers editing impact insights like contribution counts and article view trends, and customization options to improve in-app experience for users. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, a bug that prevented [[mw:Special:MyLanguage/Extension:DiscussionTools|DiscussionTools]] from working on mobile has now been fixed, restoring full functionality. [https://phabricator.wikimedia.org/T415303] '''Updates for technical contributors''' * The [[m:Special:GlobalWatchlist|Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] that makes this possible continues to improve. The latest upgrade is the inclusion of a [[mw:Extension:GlobalWatchlist#hook|new hook]], <code dir=ltr>ext.globalwatchlist.rebuild</code>, which fires after each watchlist rebuild. This allows you to run gadgets and user scripts for the Special page. [https://phabricator.wikimedia.org/T275159] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.17|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/09|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W09"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 février 2026 à 20:03 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30119102 --> == Actualités techniques n° 2026-10 == <section begin="technews-2026-W10"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/10|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments|mode Anniversaire]] Wikipedia 25 est maintenant disponible sur Wikipédia en français, anglais, betawi, breton, chinois, espagnol, gorontalo, indonésien, italien, luxembourgeois, madurais, néerlandais, sicilien, tchèque, thaï et vietnamien ! Cette campagne à temps limitée célèbre 25 ans de Wikipédia avec une mascotte : « Baby Globe », disponible sous la forme d'un réglage. Lorsque ce réglage est activé, Baby Globe est montrée sur [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments/article configuration|environ 2 500 articles]], attendant d'être découverte par des lecteurs. Chaque communauté peut choisir d'activer le mode Anniversaire par consensus et en demandant à un administrateur de le rendre disponible et de le personaliser via une [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments#Community Configuration Demo|configuration]] sur le wiki local. '''Actualités pour la contribution''' * Le [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|sous-référencement]], une nouvelle fonctionalité pour réutiliser des références avec des détails différents est maintenant disponible sur Wikipédia en suédois, polonais et [[:phab:T418209|quelques autres]]. Vous pouvez [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#test|essayer la fonctionalité]] sur ces projets ou sur testwiki et [https://en.wikipedia.beta.wmcloud.org/wiki/Sub-referencing betawiki]. Les retours des premiers essais sur Wikipédia en allemand ont été [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing/Learnings|publiés dans un rapport]]. Contactez l'équipe de Wikimédia Allemagne si vous êtes [[:m:Talk:WMDE Technical Wishes/Sub-referencing#Pilot wikis|intéressés pour devenir un wiki pilote]]. * La [[mw:Special:MyLanguage/Help:Edit check#Paste check|vérification du collage clavier]] sera disponible sur tous les Wikipédias cette semaine. Cette fonctionalité avertit les nouveaux contributeurs qui collent du texte qu'ils n'ont probablement pas écrit de vérifier si laisser celui-ci risque de causer une violation du droit d'auteur. La vérification du collage clavier [[mw:Special:MyLanguage/Edit check/Tags|marque]] toutes les modifications où l'avertissement a été montré pour permettre leur vérification. Les administrateurs locaux peuvent configurer les différents aspects de cette fonctionalité à travers [[{{#special:EditChecks}}]]. Des [[mw:Special:MyLanguage/Edit check/Paste Check#A/B Experiment|études]] sur 22 wikis ont montré que cette vérification permet une réduction de 18% des annulations comparé au groupe de contrôle. Les traducteurs peuvent [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-visualeditor-ve-mw-editcheck&filter=&optional=1&action=translate aider à traduire] cette fonctionalité. * <span lang="en" dir="ltr" class="mw-content-ltr">The [[mw:Special:MyLanguage/Readers/Reader Experience|Reader Experience team]] will be standardizing the user menu in the top right for all mobile users so that it is closer to the desktop experience. Currently this user menu is only visible to users with Advanced Mobile Controls (AMC) turned on. The only change is that a couple buttons previously in the left-side menu will move to the top right for users who do not have AMC turned on. This change is expected to go out March 9 and seeks to improve the user interface.</span> [https://phabricator.wikimedia.org/T413912] * À partir de la semaine du 2 mars, les emails envoyés lorsqu'une adresse email a été ajoutée, supprimée ou changée pour un compte changera pour adopter un formattage HTML beaucoup plus agréable et plus clair que le texte brut précédent. [https://phabricator.wikimedia.org/T410807] * Les notifications sont actuellement limitées à 2 000 entrées historiques par utilisateur et remontent à 2013 lorsque la fonctionnalité a été publiée. Le système va être modifié pour ne stocker que les notifications des 5 dernières années, mais jusqu'à 10 000 d'entre elles. Cela contribuera à la santé à long terme des infrastructures et à empêcher que les notifications plus récentes disparaissent trop tôt. [https://phabricator.wikimedia.org/T383948] * <span lang="en" dir="ltr" class="mw-content-ltr">The [[m:Special:GlobalWatchlist|Global Watchlist]] which lets you view your watchlists from multiple wikis on a single page continues to see improvements. The latest update improves label usage experience. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] now allows activating the [[mw:Special:MyLanguage/Manual:Language#Fallback languages|language fallback system]] for Wikidata items without labels in the viewed language, and showing those labels in the user’s preferred Wikidata language if no <code dir=ltr>uselang=</code> URL parameter is provided.</span> [https://phabricator.wikimedia.org/T373686][https://phabricator.wikimedia.org/T416111] * L'équipe Wikipédia Android a commencé un test beta de la [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|recherche hybride]] sur Wikipédia en grec. Cette recherche hybride supporte les requêtes sémantique et par mot clés, permettant aux utilisateurs de trouver ce qu'ils cherchent plus facilement. * Pour des raisons de sécurité, les membres de certains groupes sont [[m:Special:MyLanguage/Mandatory two-factor authentication for users with some extended rights|forcés d'avoir la double authentification]] (A2F) d'activée. Actuellement, l'A2F n'est nécessaire que pour utiliser les droits du groupe, et non pour en faire partie. Vu que ce système admet certaines failles, il sera [[phab:T418580|changé graduellement en mars]]. Les membres de ces groupes ne pourront plus désactiver la dernière méthose d'A2F sur leur compte, et il sera impossible d'ajouter des utilisateurs sans A2F à ces groupes. Il sera toujours possible de rajouter d'autres méthodes d'authentification et d'en enlever, tant qu'une est toujours activée. Dans la seconde moitié de mars, les utilisateurs sans A2F seront retirés de ces groupes. Cela s'applique aux administrateurs CentralNotice, aux vérificateurs d'utilisateurs, aux administrateurs d'interface, aux masqueurs, aux staff de Wikidata et Wikifonctions ainsi qu'aux bureaux IT et Confiance et sécurité de la WMF. Rien ne changera pour les autres utilisateurs. Voir la tâche liée pour le calendrier de déploiement. [https://phabricator.wikimedia.org/T418580] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, le problème empêchant les utilisateurs de créer une instance dans [https://www.wikibase.cloud/ Wikibase.cloud] a maintenant été résolu. [https://phabricator.wikimedia.org/T416807] '''Actualités pour la contribution technique''' * <span lang="en" dir="ltr" class="mw-content-ltr">To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], over the next month the Wikimedia Foundation will implement global API rate limits across our APIs. In early March, stricter limits will be applied to unidentified requests from outside Toolforge/WMCS and API requests that are made from web browsers. In April, higher limits will be applied to identified traffic. These limits are intentionally set as high as possible to minimise impact on the community. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]].</span> * <span lang="en" dir="ltr" class="mw-content-ltr">The Wikidata Query Service Linked Data Fragment (LDF) endpoint will be decommissioned in February. This endpoint served limited traffic, which was successfully migrated to other data access methods that were better suited to support existing use cases. The hardware used to support the LDF endpoint will be reallocated to support the ongoing backend migration efforts.</span> [https://phabricator.wikimedia.org/T415696] * Le nouvel analyseur syntaxique Parsoid [[mw:Special:MyLanguage/Parsoid/Parser Unification/Updates|continue d'être déployés sur plus de wikis]], améliorant la pérennité de la platforme et rendant plus facile l'ajout de nouvelles fonctionalités de lecture et de modification. Parsoid est maintenant l'analyseur par défaut sur 488 wikis de la WMF (268 Wikipédias), couvrant plus de 10% de toutes les lectures de pages Wikipédia. * Le processus et les critères pour [[Special:MyLanguage/Wikimedia Enterprise#Access|demander un accès exceptionnel]] au flux à fort volume de l'API ''Wikimédia Entreprise'' (sans coût pour des utilisations en rapport à notre mission) [[m:Talk:Wikimedia Enterprise#Exceptional access criteria|ont maintenant été publiés]]. Notre but est de donner une documentation plus claire et plus complète aux utilisateurs. * [https://techblog.wikimedia.org/ Le blog Tech], dédié à la communité technique de Wikimédia [https://techblog.wikimedia.org/2026/02/24/a-tech-blog-diff/ va migrer] vers [[diffblog:|Diff]], le blog pour les nouvelles et événements de la communauté. La migration devrait être terminée en Avril 2026, après quoi les nouveaux posts seront acceptés pour être publiés. Les lecteurs pourront lire les posts - anciens ou nouveaux - sur https://diff.wikimedia.org/. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.18|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/10|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W10"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 mars 2026 à 18:51 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30137798 --> == <span lang="en" dir="ltr">Tech News: 2026-11</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W11"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/11|Translations]] are available. '''Weekly highlight''' * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. * Last week, all wikis had 2 hours of read-only time, and extended unavailability for user-scripts and gadgets. This was due to a security incident which has since been resolved. Work is ongoing to prevent re-occurrences. For current information please see the [[m:Steward's noticeboard#Statement on Meta about today's user script security incident|post on the Stewards' noticeboard]] ([[m:Special:MyLanguage/Wikimedia Foundation/Product and Technology/Product Safety and Integrity/March 2026 User Script Incident|translations]]). '''Updates for editors''' * Users facing multiple blocks on mobile will now see the reasons for each block separately, instead of a generic message. This helps them understand why they are blocked and what steps they can take to resolve the issue. For example, users affected for using common VPNs (such as [[Special:MyLanguage/Apple iCloud Private Relay|iCloud Private Relay]]) will receive clearer guidance on what they need to do to start editing again. [https://phabricator.wikimedia.org/T357118] * Later this week, [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|Suggestion Mode]] will become available as a beta feature within the visual editor at all Wikipedias. This feature proactively suggests various types of actions that people can consider taking to improve Wikipedia articles, and learn about related guidelines. The feature is locally configurable, and can also be locally expanded with custom Suggestions. Current settings can be seen at [[Special:EditChecks]] and there are [[mw:Special:MyLanguage/Help:Suggestion mode#For administrators %E2%80%93 local customization|instructions for how administrators can customize]] the links to point to local guidelines. The feature is connected to [[mw:Special:MyLanguage/Help:Edit check|Edit check]] which suggests improvements while someone is writing new content. In the future, the Editing team plans to evaluate the feature's impact with newcomers through a controlled experiment. [https://phabricator.wikimedia.org/T404600] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:23}} community-submitted {{PLURAL:23|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the issue where the cursor became misaligned during the use of CodeMirror’s syntax highlighting, which makes wikitext and code easier to read, has now been fixed. This problem specifically affected users who defined a font rule in a custom stylesheet while creating a new topic with DiscussionTools. [https://phabricator.wikimedia.org/T418793] '''Updates for technical contributors''' * API rate limiting update: To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], global API rate limits will be applied this week to requests without a compliant User-Agent that originate from outside Toolforge/WMCS and to unauthenticated requests made from web browsers. Higher limits will be applied to identified traffic in April. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]]. * The new GraphQL API has been released. The API was developed as a flexible alternative to select features of the Wikidata Query Service (WDQS), to improve developer experience and foster adaptability, and efficient data access. Try it out and [[d:Wikidata:Wikibase GraphQL#Feedback and development|give feedback]]. You can also [https://greatquestion.co/wikimediadeutschland/GraphQLAPI/apply sign up for usability tests]. * The [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group|PTAC Unsupported Tools Working Group]] continued improvements to [[commons:Special:MyLanguage/Commons:Video2commons#|Video2Commons]] in February, with fixes addressing authentication errors, large-file handling, task queue visibility, and clearer upload behavior. Work is still ongoing in some areas, including changes related to deprecated server-side uploads. Read [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group#February 2026|this update]] to learn more. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.19|MediaWiki]] '''In depth''' * The Article Guidance team invites experienced Wikipedia editors from selected [[mw:Special:MyLanguage/Article guidance/Pilot wikis and collaborators#Collaborators|pilot wikis]] and interested contributors from other Wikipedias to fill out this questionnaire which is available in [https://docs.google.com/forms/d/e/1FAIpQLSfmLeVWnxmsCbPoI_UF2jyRcn73WRGWCVPHzerXb4Cz97X_Ag/viewform English], [https://docs.google.com/forms/d/e/1FAIpQLSd6rzr4XXQw8r4024fE3geTPFe13M_6w7Mitj-YJi0sOlWTAw/viewform?usp=header Arabic], [https://docs.google.com/forms/d/e/1FAIpQLSdok3-RfB18lcugYTUMGkpwmqG_8p760Wv4dCXitOXOszjUDw/viewform?usp=header Bengali], [https://docs.google.com/forms/d/e/1FAIpQLSfjTfYp4jEo0akA4B1e-Nfg3QZPCudUjhJzHzzDi6AHyAaMGA/viewform?usp=header Japanese], [https://docs.google.com/forms/d/e/1FAIpQLScteVoI29Aue4xc72dekk-6RYtvmMgQxzMI900UOawrFrSTWg/viewform?usp=header Portuguese], [https://docs.google.com/forms/d/e/1FAIpQLSetdxnYwL3ub2vqA7awCg5hJZPMIYcDPaiTe12rY9h0GYnVlw/viewform?usp=header Persian], and [https://docs.google.com/forms/d/e/1FAIpQLScNvfJF-Ot-4pzA4qAN771_0QDJ4Li19YcUsaTgSKW8Nc7U_Q/viewform?usp=header Turkish]. Your answers will help the team customize guidance for less experienced editors and help them learn community policies and practices while creating an article. Learn more [[mw:Special:MyLanguage/Article guidance|on the project page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/11|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W11"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 9 mars 2026 à 19:52 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30213008 --> == <span lang="en" dir="ltr">Tech News: 2026-12</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W12"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/12|Translations]] are available. '''Updates for editors''' * The [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature, also known as [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror 6]], has been used for wikitext syntax highlighting since November 2024. It will be promoted out of beta by May 2026 in order to bring improvements and new [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Features|features]] to all editors who use the standard syntax highlighter. If you have any questions or concerns about promoting the feature out of beta, [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|please share]]. [https://phabricator.wikimedia.org/T259059] * Some changes to local user groups are performed by stewards on Meta-Wiki and logged there only. Now, interwiki rights changes will be logged both on Meta-Wiki and the wiki of the target user to make it easier to access a full record of user's rights changes on a local wiki. Past log entries for such changes will be backfilled in the coming weeks. [https://phabricator.wikimedia.org/T6055] * On wikis using [[m:Special:MyLanguage/Flagged Revisions|Flagged Revisions]], the number of pending changes shown on [[{{#Special:PendingChanges}}]] previously counted pages which were no longer pending review, because they have been removed from the system without being reviewed, e.g. due to being deleted, moved to a different namespace, or due to wiki configuration changes. The count will be correct now. On some wikis the number shown will be much smaller than before. There should be no change to the list of pages itself. [https://phabricator.wikimedia.org/T413016] * Wikifunctions composition language has been rewritten, resulting in a new version of the language. This change aims to increase service stability by reducing the orchestrator's memory consumption. This rewrite also enables substantial latency reduction, code simplification, and better abstractions, which will open the door to later feature additions. Read more about [[f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-11|the changes]]. * Users can now sort search results alphabetically by page title. The update gives an additional option to finding pages more easily and quickly. Previously, results could be sorted by Edit date, Creation date, or Relevance. To use the new option, open 'Advanced Search' on the search results page and select 'Alphabetically' under 'Sorting Order'. [https://phabricator.wikimedia.org/T403775] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented UploadWizard on Wikimedia Commons from importing files from Flickr has now been fixed. [https://phabricator.wikimedia.org/T419263] '''Updates for technical contributors''' * A new special page, [[{{#special:LintTemplateErrors}}]], has been created to list transcluded pages that are flagged as containing lint errors to help users discover them easily. The list is sorted by the number of transclusions with errors. For example: [[{{#special:LintTemplateErrors}}/night-mode-unaware-background-color]]. [https://phabricator.wikimedia.org/T170874] * Users of the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature have been using [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] instead of [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] for syntax highlighting when editing JavaScript, CSS, JSON, Vue and Lua content pages, for some time now. Along with promoting CodeMirror 6 out of beta, the plan is to replace CodeEditor as the standard editor for these content models by May 2026. [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|Feedback or concerns are welcome]]. [https://phabricator.wikimedia.org/T419332] * The [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] JavaScript modules will soon be upgraded to CodeMirror 6. Leading up to the upgrade, loading the <code dir=ltr>ext.CodeMirror</code> or <code dir=ltr>ext.CodeMirror.lib</code> modules from gadgets and user scripts was deprecated in July 2025. The use of the <code dir=ltr>ext.CodeMirror.switch</code> hook was also deprecated in March 2025. Contributors can now make their scripts or gadgets compatible with CodeMirror 6. See the [[mw:Special:MyLanguage/Extension:CodeMirror#Gadgets and user scripts|migration guide]] for more information. [https://phabricator.wikimedia.org/T373720] * The MediaWiki Interfaces team is expanding coverage of REST API module definitions to include [[mw:Special:MyLanguage/API:REST API/Extensions|extension APIs]]. REST API modules are groups of related endpoints that can be independently managed and versioned. Modules now exist for [https://phabricator.wikimedia.org/T414470 GrowthExperiments] and [https://phabricator.wikimedia.org/T419053 Wikifunctions] APIs. As we migrate extension APIs to this structure, documentation will move out of the main MediaWiki OpenAPI spec and REST Sandbox view, and will instead be accessible via module-specific options in the dropdown on the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox] (i.e., [[{{#Special:RestSandbox}}]], available on all wiki projects). * The [[mw:Special:MyLanguage/Extension:Scribunto|Scribunto]] extension provides different pieces of information about the wiki where the module is being used via the [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual|mw.site]] library. Starting last week, the library also provides a [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#mw.site.wikiId|way]] of accessing the [[mw:Special:MyLanguage/Manual:Wiki ID|wiki ID]] that can be used to facilitate cross-wiki module maintenance. [https://phabricator.wikimedia.org/T146616] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.20|MediaWiki]] '''In depth''' * The [[m:Special:MyLanguage/Coolest Tool Award|2026 Coolest Tool Award]] celebrating outstanding community tools, is now open for nominations! Nominate your favorite tool using the [https://wikimediafoundation.limesurvey.net/435684?lang=en nomination survey] form by 23 March 2026. For more information on privacy and data handling, please see the [[foundation:Special:MyLanguage/Legal:Coolest_Tool_Award_2026_Survey_Privacy_Statement|survey privacy statement]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/12|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W12"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 mars 2026 à 20:35 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30260505 --> == <span lang="en" dir="ltr">Upcoming deployment of CampaignEvents extension to Wikibooks</span> == <div lang="en" dir="ltr"> <section begin="message"/> Hello everyone, We are writing to inform you that the [[mw:Help:Extension:CampaignEvents|CampaignEvents extension]] will be deployed to all Wikibooks projects during the week of '''23 March 2026'''. This follows last year’s broader rollout across Wikimedia projects. We realized that Wikibooks was not included at the time, and we’re now addressing that to ensure consistency across all communities. The CampaignEvents extension provides tools to support event and campaign organization on-wiki, including features like on-wiki event registration and collaboration lists(global event list). We welcome any questions, feedback, or concerns you may have. We are also happy to support anyone interested in trying out the tools. ''Apologies if this message is not in your preferred language. If you’re able to help translate it for your community, please feel free to do so.'' <section end="message"/> </div> <bdi lang="en" dir="ltr">[[User:Udehb-WMF|Udehb-WMF]] ([[User talk:Udehb-WMF|discussion]]) 19 mars 2026 à 19:22 (CET)</bdi> <!-- Message envoyé par User:Udehb-WMF@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Udehb-WMF/sandbox/MM_target&oldid=30284073 --> == <span lang="en" dir="ltr">Tech News: 2026-13</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W13"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/13|Translations]] are available. '''Weekly highlight''' * Wikimedia site users can now log in without a password using passkeys. This is a secure method supported by fingerprint, facial recognition, or PIN. With this change, all users who opt for passwordless login will find it easier, faster, and more secure to log in to their accounts using any device. The new passkey login option currently appears as an autofill suggestion in the username field. An additional [[phab:T417120|"Log in with passkey" button]] will soon be available for users who have already registered a passkey. This update will improve security and user experience. The [[c:File:Passwordless_login_screencast.webm|screen recording]] demonstrates the passwordless login process step by step. * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. '''Updates for editors''' * Wikimedia site users can now export their notifications older than 5 years using a [[toolforge:echo-chamber|new Toolforge tool]]. This will ensure that users retain their important notifications and avoid them being lost based on the planned change to delete notifications older than 5 years, as previously announced. [https://phabricator.wikimedia.org/T383948] * Wikipedia editors in Indonesian, Thai, Turkish, and Simple English now have access to Special:PersonalDashboard. This is an [[mw:Special:MyLanguage/Moderator Tools/Dashboard|early version of an experience]] that introduces newer editors to patrolling workflows, making it easier for them to move from making edits to participating in more advanced moderation work on their project. [https://phabricator.wikimedia.org/T402647] * The [[Special:Block]] now has two minor interface changes. Administrators can now easily perform indefinite blocks through a dedicated radio button in the expiry section. Also, choosing an indefinite expiry provides a different set of common reasons to select from, which can be changed at: [[MediaWiki:Ipbreason-indef-dropdown]]. [https://phabricator.wikimedia.org/T401823] * Mobile editors [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#Logged-out|at several wikis]] can now see an improved logged-out edit warning, thanks to the recent updates from the Growth team. These changes released last week are part of ongoing efforts and tests to enhance [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|account creation experience on mobile]] and then increase participation. [https://phabricator.wikimedia.org/T408484] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:36}} community-submitted {{PLURAL:36|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented mobile web users from seeing the block information when affected by multiple blocks has been fixed. They can now see messages of all the blocks currently affecting them when they access Wikipedia. '''Updates for technical contributors''' * Images built using Toolforge will soon get the upgraded buildpacks version, bringing support for newer language versions and other upstream improvements and fixes. If you use Toolforge Build Service, review the recent [https://lists.wikimedia.org/hyperkitty/list/cloud-announce@lists.wikimedia.org/thread/EMYTA32EV2V5SQ2JIEOD2CL66YFIZEKV/ cloud-announce email] and update your build configuration as necessary to ensure your tools are compatible. [https://wikitech.wikimedia.org/w/index.php?title=Help:Toolforge/Building_container_images&oldid=2392097#Buildpack_environment_upgrade_process][https://phabricator.wikimedia.org/T380127] * The [https://api.wikimedia.org/wiki/Main_Page API Portal] documentation wiki will shut down in June 2026. API keys created on the API Portal will continue to work normally. api.wikimedia.org endpoints will be deprecated gradually starting in July 2026. Documentation on the API Portal is moving to [[mw:Wikimedia APIs|mediawiki.org]]. Learn more on the [[wikitech:API Portal/Deprecation|project page]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.21|MediaWiki]] '''In depth''' * [[m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]] is considering improvements to [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names|automatically generated reference names in VisualEditor]]. Please check out the [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names#Proposed solutions|proposed solutions]] and participate in the [[m:Talk:WMDE Technical Wishes/References/VisualEditor automatic reference names#Request for comment|request for comment]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/13|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W13"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 mars 2026 à 17:51 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30268305 --> == Actualités techniques n° 2026-14 == <section begin="technews-2026-W14"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/14|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le version Beta de [[abstract:|Abstract Wikipedia]], un nouveau projet Wikimédia indépendant du langage, a été lancée la semaine dernière. Ce projet permet aux communautés de construire des articles Wikipédia dans leur langue natale, qui peuvent directement être lus par les autres utilisateurs et utilisatrices dans leur propre langage. Le wiki fonctionne grâce à des instructions de Wikifunctions et au contenu structuré issu de Wikidata. [[:f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-26|En savoir plus]]. '''Actualités pour la contribution''' * L'équipe Croissance mène un test A/B afin d'évaluer l'effet d'un message plus clair et plus convivial encourageant à la création de comptes sur les wikis. Actuellement, lorsqu'un utilisateur mobile non connecté lance la modification, un message d'avertissement s'affiche, pouvant paraître abrupt et décourageant. Il présente également la modification par compte temporaire comme option par défaut, au lieu d'inciter à la création d'un compte. Le test est mené sur dix Wikipédia, dont les versions en arabe, français, espagnol et allemand. [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#2. Improve logged-out warning message (T415160)|En savoir plus]]. * L'équipe des applications Wikimédia sollicite vos commentaires sur [[mw:Special:MyLanguage/Wikimedia Apps/Team/Future of Editing on the Mobile Apps|comment devrait fonctionner l'édition dans les applications mobiles Wikipédia]]. La discussion porte sur l'amélioration de l'accès aux outils d'édition lorsque les utilisateurs appuient sur « Modifier ». Cette initiative s'inscrit dans un effort plus large visant à offrir aux lecteurs intéressés par la contribution une expérience utilisateur plus intuitive. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:45|la tâche soumise|les {{formatnum:45}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:45||s}} la semaine dernière]]. Par exemple, un problème avec la récupération de citations à partir du site d'archive de journaux [https://www.newspapers.com Newspapers.com], qui ne fonctionnait plus en raison d'un blocage des requêtes [[mw:Special:MyLanguage/Citoid|Citoid]], a maintenant été résolu. [https://phabricator.wikimedia.org/T419903] '''Actualités pour la contribution technique''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.22|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/14|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W14"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 30 mars 2026 à 21:25 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30329462 --> == Action Required: Update templates/modules for electoral maps (Migrating from P1846 to P14226) == Hello everyone, This is a notice regarding an ongoing data migration on Wikidata that may affect your election-related templates and Lua modules (such as <code>Module:Itemgroup/list</code>). '''The Change:'''<br /> Currently, many templates pull electoral maps from Wikidata using the property [[:d:Property:P1846|P1846]], combined with the qualifier [[:d:Property:P180|P180]]: [[:d:Q19571328|Q19571328]]. We are migrating this data (across roughly 4,000 items) to a newly created, dedicated property: '''[[:d:Property:P14226|P14226]]'''. '''What You Need To Do:'''<br /> To ensure your templates and infoboxes do not break or lose their maps, please update your local code to fetch data from [[:d:Property:P14226|P14226]] instead of the old [[:d:Property:P1846|P1846]] + [[:d:Property:P180|P180]] structure. A [[m:Wikidata/Property Migration: P1846 to P14226/List|list of pages]] was generated using Wikimedia Global Search. '''Deadline:'''<br /> We are temporarily retaining the old data on [[:d:Property:P1846|P1846]] to allow for a smooth transition. However, to complete the data cleanup on Wikidata, the old [[:d:Property:P1846|P1846]] statements will be removed after '''May 1, 2026'''. Please update your modules and templates before this date to prevent any disruption to your wiki's election articles. Let us know if you have any questions or need assistance with the query logic. Thank you for your help! [[User:ZI Jony|ZI Jony]] using [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 3 avril 2026 à 19:11 (CEST) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29941252 --> == Actualités techniques n° 2026-15 == <section begin="technews-2026-W15"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/15|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L’[[mw:Special:MyLanguage/Help:Extension:CampaignEvents|extension CampaignEvents]] comprend désormais une nouvelle fonctionnalité de définition d’objectifs de groupe, permettant aux organisateurs de définir et de suivre les objectifs de l’événement, tels que le nombre d’articles créés et de contributeurs participants en temps réel. De même, les participants peuvent travailler vers des cibles communes et voir leur impact collectif au fur et à mesure que l’événement se déroule. Cette fonctionnalité est désormais disponible sur tous les wikis Wikimedia. Pour en savoir plus, consultez [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Collaborative contributions#Goal setting|la documentation]]. * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] La nouvelle fonctionnalité d'[[mw:Special:MyLanguage/Help:Watchlist labels|étiquettes de liste de suivi]] (annoncée dans les [[m:Special:MyLanguage/Tech/News/2026/07|Actualités techniques 2026-07 ]]) est désormais disponible via l'ÉditeurVisuel, l'éditeur de code et l'«étoile de suivi»(ou le lien de suivi, pour les habillages qui n'ont pas d'icône d'étoile). Auparavant, il n'était possible d'attribuer des étiquettes que via [[Special:EditWatchlist|Modifier la liste de suivi]]. Dans ces trois emplacements, il s'agit d'un nouveau champ situé après le champ d'expiration. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. Par exemple, le problème où les pages de discussion sur mobile avec Parsoid sont inutilisables après les en-têtes de section vides, a maintenant été résolu. [https://phabricator.wikimedia.org/T419171] '''Actualités pour la contribution technique''' * La [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|fonctionnalité de sous-référencement]], qui permet aux contributeurs d'ajouter des détails à une référence existante sans la dupliquer, sera progressivement déployée sur [[phab:T414094|davantage de wikis]] plus tard cette année. Les wikis utilisant le gadget [[mw:Special:MyLanguage/Reference Tooltips|Reference Tooltips]] sont encouragés à mettre à jour leur version (généralement sur [[m:MediaWiki:Gadget-ReferenceTooltips.js|MediaWiki:Gadget-ReferenceTooltips.js]] comme indiqué [https://en.wikipedia.org/w/index.php?diff=1344408362 ici]) pour assurer la compatibilité. D'autres gadgets liés aux références pourraient également être affectés. [https://phabricator.wikimedia.org/T416304] * Toutes les éditions de Wikinews seront fermées et passeront en mode lecture seule le 4 mai 2026. Le contenu restera accessible, mais aucune nouvelle modification ni aucun nouvel article ne pourra être ajouté. Cette fermeture a été approuvée par le Conseil d'administration de la Fondation Wikimedia à la suite de discussions prolongées. [[m:Wikimedia Foundation Board noticeboard#Board of Trustees Approves Closure of Wikinews|En savoir plus]]. * L'[[:mw:Special:MyLanguage/API:Action API|API d'action]] a proposé plusieurs formats pour les résultats demandés. L'un d'entre eux, <bdi lang="zxx" dir="ltr"><code><nowiki>format=php</nowiki></code></bdi>, sera bientôt supprimé. Veuillez vous assurer que vos scripts ou robots utilisent le [[mw:Special:MyLanguage/API:Data formats#Output|format JSON]]. Cette suppression devrait affecter très peu de scripts et de robots. [https://phabricator.wikimedia.org/T118538] * La page [[Special:NamespaceInfo|Special:NamespaceInfo]] inclut désormais les alias d'espace de noms. Par exemple «WP» pour l'espace de noms ''Projet'' (''Wikipédia'') sur la Wikipédia en allemand. [https://phabricator.wikimedia.org/T381455] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.23|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/15|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W15"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 6 avril 2026 à 18:19 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30362761 --> == <span lang="en" dir="ltr">Tech News: 2026-16</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W16"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/16|Translations]] are available. '''Weekly highlight''' * Experienced editors are invited to [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Main_Page test] the [[mw:Special:MyLanguage/Article guidance|Article guidance]] feature, designed to help less-experienced editors create well-structured, policy-compliant Wikipedia articles. Testing instructions are [[mw:Special:MyLanguage/Article guidance/Test feature guide|available]]. Also, after reviewing [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Category:Pages_using_article_guidance the outlines], please provide feedback on the [[mw:Talk:Article guidance|project talk page]]. Based on your input, the feature will be refined and transferred to the pilot Wikipedias to translate and adapt. Check out [[c:File:Article Guidance workflow demo - April 2026.webm|the video]] explaining the feature. '''Updates for editors''' * On most wikis, all autoconfirmed users can now use [[Special:ChangeContentModel|Special:ChangeContentModel]] page to [[mw:Special:MyLanguage/Help:ChangeContentModel|create new pages with custom content models]], such as mass message lists, making custom page formats more accessible. Check [[Special:ListGroupRights|Special:ListGroupRights]] for the status of your wiki. [https://phabricator.wikimedia.org/T248294] * The Growth team has launched an [[mw:Special:MyLanguage/Contributors/Account_Creation_Experiments|account creation experiment]] to evaluate whether adding an account creation button to the mobile web header increases new account registrations and encourages more mobile users to contribute to the wikis. The experiment is currently live on Hindi, Indonesian, Bengali, Thai, and Hebrew Wikipedia, and targets 10% of logged-out mobile web users. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:30}} community-submitted {{PLURAL:30|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where VisualEditor could get stuck loading on Windows devices with animations turned off, has now been fixed. [https://phabricator.wikimedia.org/T382856] '''Updates for technical contributors''' * Starting later this week, {{int:group-abusefilter}} who have the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature enabled will have [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] instead of [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] as the editor at [[Special:AbuseFilter|Special:AbuseFilter]]. This is part of the broader effort to make the user experience more consistent across all editors. [https://phabricator.wikimedia.org/T399673][https://phabricator.wikimedia.org/T419332] * Tools and bots that access the [[mw:Special:MyLanguage/Notifications/API|Notifications API]] (<bdi lang="zxx" dir="ltr"><code><nowiki>action=query&meta=notifications</nowiki></code></bdi>) will need to update their OAuth or BotPassword grants to also include access to private notifications. [https://phabricator.wikimedia.org/T421991] * Due to a library upgrade, listings on category pages may be displayed out of order starting on Monday, 20th April. A migration script will be run to correct this, and will take hours to days depending on the size of the wiki (up to a week for English Wikipedia). [https://phabricator.wikimedia.org/T422544] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.24|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/16|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W16"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 13 avril 2026 à 17:19 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30380527 --> == Actualités techniques n° 2026-17 == <section begin="technews-2026-W17"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/17|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Après deux ans de développement, la version [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]], également connue sous le nom de [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror 6]], sortira de sa phase bêta le mardi 21 avril. Elle offrira une meilleure lisibilité du code et du wikitext, une réduction des fautes de frappe et d'autres [[mw:Special:MyLanguage/Help:Extension:CodeMirror|avantages]] à tous les utilisateurs du surligneur de syntaxe standard. Un grand merci au bénévole [https://phabricator.wikimedia.org/p/Bhsd/ Bhsd] qui a développé de nombreuses nouvelles fonctionnalités, notamment [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Code folding|le repliement de code]], [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Autocompletion|la saisie semi-automatique]] et [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Linting|l'analyse statique du code]]. [https://phabricator.wikimedia.org/T259059] * Une mise à jour majeure de l'application Wikipédia pour iOS est en cours de déploiement, en restructurant l'interface pour s'harmoniser avec le tout nouveau design visuel "Liquid Glass" d'Apple. [https://apps.apple.com/us/app/wikipedia/id324715238 Télécharger la dernière version] et découvrez les nouveautés. '''Actualités pour la contribution''' * [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|Les listes de lecture]] est une fonctionnalité qui permet aux lecteurs d'enregistrer des articles dans une liste pour les lire ultérieurement. Cette fonctionnalité est actuellement en version bêta sur les Wikipédias en arabe, français, indonésien, vietnamien et chinois, et activée par défaut pour tous les nouveaux comptes sur toutes les Wikipédias. * Une expérimentation visant à étendre [[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews|les aperçus de page au web mobile]] sera lancée la semaine du 20 avril sur les versions arabe, anglaise, française, italienne, polonaise et vietnamienne de Wikipédia. Les aperçus de page sont des fenêtres contextuelles affichant une miniature, un premier paragraphe et un lien bleu permettant d'ouvrir l'article complet, facilitant ainsi la découverte de contenu. Cette fonctionnalité est déjà disponible sur ordinateur et dans les applications. [[m:Special:MyLanguage/List of experiments in Product and Technology#Template|En savoir plus sur cette expérimentation et d'autres]]. * Sur plusieurs wikis, les contributeurs connectés qui n'ont pas [[mw:Special:MyLanguage/Help:Email confirmation|confirmé leur adresse électronique]] peuvent désormais voir une bannière les invitant à le faire. La confirmation de l'adresse électronique permet à un utilisateur de récupérer l'accès à son compte en cas de perte. [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security#Encouraging users to confirm their email addresses|En savoir plus]]. [https://phabricator.wikimedia.org/T421366] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:15|la tâche soumise|les {{formatnum:15}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:15||s}} la semaine dernière]]. Par exemple, un problème qui entraînait des ralentissements lors de la modification de très grandes pages wiki dans l'éditeur wikitext de 2017, des problèmes de chargement, de prévisualisation et de défilement, ainsi que des problèmes de performance lors de la sélection, de la découpe ou du collage de contenu, a maintenant été résolu. [https://phabricator.wikimedia.org/T184857] '''Actualités pour la contribution technique''' * Dans le cadre de la promotion de [[mw:Special:MyLanguage/Help:Extension:CodeMirror|CodeMirror]] à partir d'une fonctionnalité bêta, tous les utilisateurs se serviront de [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] au lieu de [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] pour la coloration syntaxique lors de l'édition de pages de contenu JavaScript, CSS, JSON, Vue et Lua. [https://phabricator.wikimedia.org/T419332] * <span class="mw-translate-fuzzy">Le service <code>mirrors.wikimedia.org</code> pour les utilisateurs de Debian et Ubuntu sera définitivement arrêté le 15 mai. Le matériel du serveur sera remplacé par des solutions plus performantes. Certains utilisateurs devront peut-être migrer vers un autre serveur qui ne devra prendre qu'une minute. [https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/LJYRIS4WB66HIRCAO4GIDTXCMDVZRBMA/ Vous pouvez en savoir plus].</span> [https://phabricator.wikimedia.org/T416707] * Les tables <bdi lang="zxx" dir="ltr"><code><nowiki>image</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>oldimage</nowiki></code></bdi> seront supprimées de [[wikitech:Help:Wiki Replicas|wikireplicas]]. Si vos outils ou requêtes accèdent directement à <bdi lang="zxx" dir="ltr"><code><nowiki>image</nowiki></code></bdi> ou <bdi lang="zxx" dir="ltr"><code><nowiki>oldimage</nowiki></code></bdi>, veuillez les mettre à jour pour utiliser les tables <bdi lang="zxx" dir="ltr"><code><nowiki>file</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>filerevision</nowiki></code></bdi> avant le 28 mai. [https://phabricator.wikimedia.org/T28741] * Suite à la récente mise en place de limites de débit globales pour les API non identifiées, la Fondation Wikimedia poursuit ses efforts pour garantir [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|une utilisation équitable de l'infrastructure]] en appliquant des limites globales au trafic des API identifiées à partir de la dernière semaine d'avril. Ces limites sont volontairement fixées au niveau le plus élevé possible afin de minimiser l'impact sur la communauté. Les bots exécutés dans Toolforge/WMCS ou disposant des droits d'utilisateur de bot sur un wiki ne devraient pas être affectés pour le moment. Toutefois, il est conseillé à tous les développeurs de suivre les bonnes pratiques mises à jour. Pour plus d'informations, consultez la page [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|API Wikimedia/Limites de débit]] et la [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits/FAQ|Foire aux questions]]. * L'[[mw:Special:MyLanguage/Attribution API|API d'attribution]] est désormais disponible en [[mw:Special:MyLanguage/Wikimedia APIs/Stability policy|version bêta]]. Elle récupère les informations nécessaires pour créditer les articles et les fichiers multimédias de Wikimedia, quel que soit leur lieu d'utilisation. La documentation de référence est disponible sur la page dédiée au Sandbox REST, accessible sur tous les wikis Wikimedia (comme [https://en.wikipedia.org/w/index.php?api=attribution.v0-beta&title=Special%3ARestSandbox le sandbox REST de Wikipédia en anglais]). N'hésitez pas à partager vos commentaires sur la [[mw:Talk:Attribution API|page de discussion du projet]]. * Il n'y aura pas de nouvelle version de MediaWiki cette semaine. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/17|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W17"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 20 avril 2026 à 17:00 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30432763 --> == Request for comment (global AI policy) == <bdi lang="en" dir="ltr" class="mw-content-ltr"> Apologies for writing in English. {{int:Please-translate}} A [[:m:Requests for comment/Artificial intelligence policy|request for comment]] is currently being held to decide on a global AI policy. {{int:Feedback-thanks-title}} [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 26 avril 2026 à 02:57 (CEST) </bdi> <!-- Message envoyé par User:Codename Noreste@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30424282 --> == Actualités techniques n° 2026-18 == <section begin="technews-2026-W18"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/18|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Un changement dans la manière dont les utilisateurs et utilisatrices sont automatiquement confirmés est en cours pour améliorer la protection contre le vandalisme. Actuellement, il suffit d’avoir un compte depuis quelques jours avec quelques contributions pour être ajouté au groupe [[{{int:grouppage-autoconfirmed/{{CONTENTLANGUAGE}}}}|{{int:group-autoconfirmed}}]]. Cette configuration tend à être exploitée par certains vandales qui créent des comptes et commencent à les utiliser après un certain temps. Pour réduire ce problème, la configuration va changer la semaine prochaine afin que l’âge du compte minimum pour être confirmé automatiquement ne soit calculé qu’à partir de la première modification, au lieu de la date d’inscription. L’âge minimum du compte restera le même, c’est seulement le point de départ pour calculer cet âge qui change. Ce changement ne sera déployé que sur les wikis qui nécessitent au moins une contribution pour satisfaire les conditions de confirmation automatique. [https://phabricator.wikimedia.org/T418484] * Tous les utilisateurs et utilisatrices de Wikipédia avec un nouveau compte et ceux qui ont activé l’option « activer automatiquement la plupart des fonctionnalités bêta » peuvent désormais utiliser la fonctionnalité bêta de [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|listes de lecture]] pour enregistrer des articles à lire plus tard. Cela permet d’organiser les lectures qui nous intéressent à un endroit unique pour y accéder facilement. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, le problème avec les images d’infoboite qui avaient une marge intérieure immense dans Firefox a été corrigé. [https://phabricator.wikimedia.org/T423676] '''Actualités pour la contribution technique''' * Pour rappel, la limite globale d’accès à l’API sera appliquée cette semaine pour identifier le trafic de l’API. Le but est d’aider à garantir un [[mw:MediaWiki Product Insights/Responsible Reuse|accès équitable à l’infrastructure]]. Les robots qui s’exécutent dans Toolforge ou WMCS, ou avec le droit utilisateur ''robot'' sur les wikis, ne devraient pas être affectés pour le moment. Cependant, il est conseillé à tous les développeurs et développeuses de se conformer aux nouvelles bonnes pratiques à suivre. Pour plus d’informations, notamment la limite globale d’accès effective, consultez [[mw:Wikimedia APIs/Rate limits|la page sur la limite d’accès des API de Wikimedia]] et les [[mw:Wikimedia APIs/Rate limits/FAQ|questions-réponses]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.26|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/18|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W18"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 avril 2026 à 20:06 (CEST) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30458046 --> == Actualités techniques n° 2026-19 == <section begin="technews-2026-W19"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/19|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L’équipe chargée des fonctionnalités de [[mw:Special:MyLanguage/Article guidance|Guidage des articles]] invite les contributeurs et contributrices expérimentés des [[mw:Special:MyLanguage/Article guidance/Pilot wikis and collaborators|Wikipédia pilotes]] (arabe, bangla, japonais, portugais, persan, turc, anglais simplifié, espagnol et français) à contribuer à la traduction et à l’adaptation des [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Category:Pages_using_article_guidance exemples de trames d’articles]. Ces trames guideront les contributeurs dans la création d’articles clairs, bien structurés et conformes aux règles lors de l’utilisation de [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Special:NewArticle la fonctionnalité] dès son lancement en mai 2026. Des [[mw:Special:MyLanguage/Article guidance#Adapting a sample outline in a Wikipedia|instructions simples]] expliquant comment traduire et adapter ces trames sont disponibles. '''Actualités pour la contribution''' * Le [[:m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif sur les produits et les technologies]] a publié [[:m:Special:MyLanguage/Product and Technology Advisory Council/May 2026 draft PTAC recommendation for feedback|une proposition de recommandation]] d’une procédure type que les organisations affiliées à Wikimedia pourraient suivre pour contribuer au domaine technique. Les membres de la communauté sont invités à donner leur avis sur cette recommandation avant le 8 mai [[:m:Talk:Product and Technology Advisory Council/May 2026 draft PTAC recommendation for feedback|sur la page de discussion]]. * Le nombre de préférences de taille de la miniature disponibles dans MediaWiki va être réduit à trois options standardisées : ''petite'' (180 px), ''moyenne'' (250 px) et ''large'' (400 px), dans le cadre du travail en cours pour améliorer les performances et réduire la pression sur les services de miniatures. Par conséquent, les préférences existantes seront automatiquement adaptées à la nouvelle taille la plus proche (par exemple, les petites tailles comme 120 px ou 150 px s’afficheront à 180 px, tandis que les grandes tailles comme 300 px ou 360 px s’afficheront à 400 px). L’interface des préférences sera bientôt mise à jour pour refléter ces changements, et les utilisateurs qui souhaitent s’y opposer ou donner leur avis peuvent le faire. [https://phabricator.wikimedia.org/T424909] * Dorénavant, même lorsqu’une permission expire automatiquement, les utilisateurs recevront une notification Echo similaire à la notification normale pour les changements de permissions. Quant au [[m:Special:MyLanguage/Global reminder bot|robot global de rappel]], il continue de prévenir les utilisateurs une semaine ''avant'' que leurs droits ne soient sur le point d’expirer, afin qu’ils puissent les faire renouveler. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:32|la tâche soumise|les {{formatnum:32}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:32||s}} la semaine dernière]]. Par exemple, le problème du sélecteur de langue ULS dans [[m:Special:Translate|Special:Translate]] qui faisait défiler verticalement alors qu’il ne devait pas, a été résolu. Auparavant, lorsque les utilisateurs ouvraient le menu déroulant « Traduire en français » et commençaient à saisir le nom d’une langue, la boîte de dialogue défilait verticalement de quelques pixels même lorsqu'il y avait suffisamment d’espace pour afficher tous les résultats. Le menu déroulant ne se déplace plus inutilement lors du filtrage des langues. [https://phabricator.wikimedia.org/T358864] * La [[m:Special:GlobalWatchlist|liste de suivi globale]], qui vous permet de consulter vos listes de suivi provenant de plusieurs wikis sur une seule page, continue de s’améliorer. Par exemple, les listes de suivi pour les sites avec Wikibase tels que [[:d:|Wikidata]] prennent désormais en charge les éléments [[mw:Special:MyLanguage/Extension:EntitySchema|EntitySchema]] pour un meilleur suivi. Le mode Mises à jour en direct actualise désormais la page spéciale toutes les 60 secondes afin de se conformer aux [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|nouvelles limites globales d’accès à l’API]] pour une meilleure réactivité en temps réel. Par ailleurs, un bug de directionnalité du texte qui affichait les liens comme « changements 3 » au lieu de « 3 changements » dans les listes à directions mixtes a été corrigé. [https://phabricator.wikimedia.org/T415450][https://phabricator.wikimedia.org/T424422][https://phabricator.wikimedia.org/T418091] '''Actualités pour la contribution technique''' * La deuxième phase de [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|limitations globales d’accès à l’API]] a été déployée pour réduire l’[[diffblog:2026/03/26/quo-vadis-crawlers-progress-and-whats-next-on-safeguarding-our-infrastructure/|impact des robots IA]] et assurer un accès équitable et durable aux ressources de Wikimedia, en donnant la priorité au trafic humain et conforme à notre mission. Les [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits#Limits|limites]] ne s’appliquent plus par heure mais par minute, produisant une meilleure répartition dans les structures de trafic ainsi qu’une meilleure prévisibilité de la charge de l’API. Les utilisateurs de la communauté ne devraient pas être affectés, et aucune action n’est requise. Les premières indications montrent que certains requérants basés sur l'agent utilisateur ajustent leur comportement, et environ 64 % du trafic API automatisé a été identifié. La surveillance continue, et Wikimedia Enterprise reste disponible pour l’assistance commerciale. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.27|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/19|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W19"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 4 mai 2026 à 22:43 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30498077 --> == Actualités techniques n° 2026-20 == <section begin="technews-2026-W20"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/20|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * La Communauté Technique a publié [[m:Special:MyLanguage/Community Wishlist/How to write a good wish|de nouvelles directives]] expliquant comment les souhaits sur la Liste de souhaits de la communauté sont triés et priorisés. La documentation vise à aider les contributeurs à rédiger des propositions plus solides en clarifiant les facteurs qui influencent les décisions de priorisation. Au-delà du nombre de votes, les directives mettent en avant des considérations telles que l'impact potentiel sur la communauté pour déterminer quels souhaits avanceront. '''Actualités pour la contribution''' * L'équipe de croissance des lecteurs lance une expérience pour tester une nouvelle [[mw:Special:MyLanguage/Readers/Reader_Growth/Share_Card|fonctionnalité de Partage de Carte]] qui permet aux lecteurs de créer des cartes visuellement attrayantes à partir d'articles Wikipédia ou de sections d'articles sélectionnées et de les partager en ligne, chaque carte renvoyant à l'article original afin d'aider à augmenter le lectorat et la découverte des articles. Le test A/B réservé aux mobiles ne sera disponible qu'à une partie des lecteurs sur les Wikipédia en arabe, chinois, français, vietnamien et anglais afin de mieux comprendre les habitudes de lecture et de partage, et est prévu pour commencer la semaine du 18 mai pour une durée de quatre semaines. * Les applications Wikipedia pour Android et iOS ont récemment publié en version bêta le [[mw:Special:MyLanguage/Wikimedia_Apps/Team/25th_Birthday_Reading_Challenge|défi de lecture de 25 jours]], dans le cadre des efforts visant à stimuler l'engagement des lecteurs en encourageant les utilisateurs à atteindre des objectifs de lecture. Pour suivre leur série de lectures pendant le défi, les utilisateurs de l'application peuvent ajouter un widget avec Baby Globe à leur écran d'accueil. Le défi commence officiellement le 11 mai. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:17|la tâche soumise|les {{formatnum:17}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:17||s}} la semaine dernière]]. Par exemple, un problème où la préférence globale pour activer la coloration syntaxique dans le wikitexte pouvait s'éteindre de manière inattendue après avoir été activée a maintenant été corrigé. [https://phabricator.wikimedia.org/T425286] '''Actualités pour la contribution technique''' * [[File:Octicons-tools.svg|12px|link=|alt=|Sujet technique]] Le module ResourceLoader <bdi lang="zxx" dir="ltr"><code><nowiki>mediawiki.ui.input</nowiki></code></bdi>, obsolète depuis [[m:Special:MyLanguage/Tech/News/2023/39|septembre 2023]], sera supprimé cette semaine. Il existe un [[mw:Special:MyLanguage/Codex/Migrating_from_MediaWiki_UI|guide pour migrer de l’interface MediaWiki UI vers Codex]] pour tous les outils qui l’utilisent. [https://phabricator.wikimedia.org/T420125] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.2|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/20|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W20"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 11 mai 2026 à 21:20 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30524429 --> == Actualités techniques n° 2026-21 == <section begin="technews-2026-W21"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/21|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L'équipe de Wikipédia abstraite a identifié cinq wikis pilotes potentiels pour évaluer leur intérêt à adopter des articles abstraits sur leurs wikis. Les pilotes sont Wikipédia en Malayalam, en Bengali, en Dagbani, en Arabe et en Indonésien. La période de retour d'information sera ouverte jusqu'au 22 mai. Si votre communauté est intéressée à devenir un pilote, [[m:Talk:Abstract Wikipedia|faites-nous savoir sur Meta]]. '''Actualités pour la contribution''' * Une expérience visant à afficher [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|les listes de lecture]] aux lecteurs non connectés sur le web mobile sera lancée le 18 mai sur les Wikipédias Allemande, Espagnole, Italienne, Portugaise, Polonaise, Néerlandaise, Turque et Ourdou, et durera un mois. Cet effort soutient des objectifs plus larges consistant à aider les lecteurs à enregistrer et organiser des articles pour une lecture ultérieure, tout en encourageant des habitudes qui pourraient mener à de futures contributions sur Wikipédia. * Pour prendre en charge un bouton de marquage dans la fonctionnalité bêta Liste de lecture, le menu "Outils > Action" a été mis à jour pour afficher des icônes, y compris l'indicateur en forme d'étoile de suivi qui aide les éditeurs à identifier les articles suivis temporairement. Les icônes correspondent désormais également à celles utilisées sur mobile, améliorant la cohérence entre les plateformes. Le changement est actuellement limité au menu des actions et concerne principalement les éditeurs ayant des droits d'utilisateur privilégié. [https://phabricator.wikimedia.org/T426008] * [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|Mode de Suggestion]] a été publié en tant qu'[[w:en:A/B test|test A/B]] pour les nouveaux éditeurs sur le site mobile à [[phab:T421189|~15 Wikipédias]]. L'expérience mesurera l'impact que le Mode de Suggestion a sur la proportion de sessions d'édition sur le web mobile par des nouveaux éditeurs qui aboutissent à des modifications constructives (non annulées) des articles. L'expérience évaluera également l'impact de la fonctionnalité sur la rétention des éditeurs et surveillera les changements dans les taux d'annulation et de blocage. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, un problème dans l'application Android de Wikipédia où les images pourraient parfois ne pas se charger après avoir ouvert une notification de liste de lecture recommandée, a maintenant été corrigé. [https://phabricator.wikimedia.org/T418231] '''Actualités pour la contribution technique''' * L'[[mw:Special:MyLanguage/Wikidata Platform|équipe de la Plateforme Wikidata]] a publié sa [[d:Special:MyLanguage/Wikidata:SPARQL query service/WDQS backend update/Backend Replacement|recommandation de remplacement du backend]] et l'[[wikitech:Wikidata Query Service/WDQS Architecture re-design|architecture technique]] qui l'accompagne pour la migration du Wikidata Query Service (WDQS) hors de Blazegraph grap. Les retours sont attendus jusqu'au 25 mai 2026, en particulier sur les éventuelles lacunes et impacts sur les cas d'utilisation avancés. Les membres de la communauté Wikidata et les utilisateurs de WDQS sont également encouragés à aider à identifier les outils et flux de travail à fort impact qui pourraient nécessiter une attention sur [[d:Wikidata:SPARQL query service/WDQS backend update/High-Impact Use Cases|cette page]]. Les retours peuvent être partagés sur la [[d:Wikidata talk:SPARQL query service/WDQS backend update|page de discussion de la migration]] ou lors de la [[d:Special:MyLanguage/Wikidata:Blazegraph Migration Office Hours|prochaine heure de bureau]]. Voir le [[d:Special:MyLanguage/Wikidata:Wikidata Platform team/Newsletter|bulletin de l'équipe WDP]] pour plus de détails. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.3|MediaWiki]] '''En détails''' * Sur les Wikipédia en anglais, en français, en japonais et quelques autres, il y a eu un [[diffblog:2025/09/02/better-detecting-bots-and-replacing-our-captcha/|essai de hCaptcha]], un service tiers de détection de robots. L'essai a montré que hCaptcha détecte et dissuade efficacement certaines activités automatisées de mauvaise foi, à la fois par lui-même et en donnant des signaux aux [[w:en:Wikipedia:Village pump (technical)/Archive 225#Introducing SuggestedInvestigations|checkusers et stewards]] pour qu'ils enquêtent. Comme les résultats étaient positifs, hCaptcha sera déployé sur toutes les wikis au cours des prochaines semaines. [[mw:Special:MyLanguage/Product Safety and Integrity/Anti-abuse signals/hCaptcha|Voir la page du projet hCaptcha]] pour des informations techniques sur la mise en œuvre et les protections de la vie privée. [[diffblog:2026/05/04/better-detecting-bots-and-replacing-our-captcha-part-2/|En savoir plus]]. * La dernière mise à jour de la Technologie communautaire est désormais disponible, avec des progrès dans plusieurs initiatives de la Liste de souhaits communautaire, y compris l'extension des listes de lecture de l'application mobile au site web, la prise en charge de nouvelles langues pour "Who Wrote That" et le Tableau de bord personnel, des améliorations du rendu 3D et des graphiques, ainsi que des travaux à venir sur le tri des pages de discussion, la lecture audio et les flux de travail d'édition. La mise à jour partage également les priorités actuelles, les tendances de l'état de la Liste de souhaits et les opportunités de retour d'information de la communauté sur les domaines de concentration futurs et le Plan annuel 2026–2027 de la Wikimedia Foundation. [[m:Special:MyLanguage/Community Wishlist/Updates#May 13, 2026: Latest updates from the Community Tech team|Lisez le bulletin d'information complet pour plus de détails]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/21|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W21"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 18 mai 2026 à 22:21 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30539262 --> == Actualités techniques n° 2026-22 == <section begin="technews-2026-W22"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/22|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Faisant suite à une [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#LOWM|expérience fructueuse sur la création de comptes]], un message d'avertissement pour les personnes déconnectées sera déployé sur les wikis Wikimédia durant la première semaine de juin. Ce changement n'affectera que les personnes déconnectées sur l'interface web mobile qui commencent à modifier. Cette nouvelle expérience est faite pour encourager la création de comptes, tout en autorisant aux utilisateurs de modifier à l'aide de comptes temporaires. Les résultats de l'expérience ont montré une augmentation de la création de compte d'environ 27 % pour ceux ayant vu le nouveau message. Comme prévu, puisque plus de personnes créent un compte, la création de comptes temporaires a diminué de 16 %. L'expérience n'a pas montré d'autres changements sur la qualité des modifications ou sur les autres indicateurs surveillés. [https://phabricator.wikimedia.org/T424595] '''Actualités pour la contribution''' * Pour des raisons de sécurité, les membres de certains groupes d’utilisateurs sont [[m:Special:MyLanguage/Mandatory two-factor authentication for users with some extended rights|forcés d'avoir l'authentification à 2 facteurs]] (A2F) d'activée. Les membres de ces groupes seront dans l'impossibilité de désactiver la dernière méthode d'A2F sur leur compte, et il sera impossible d'ajouter des utilisateurs sans A2F à ces groupes. Ces utilisateurs auront toujours la possibilité d'ajouter ou d'enlever des nouvelles méthodes d'authentification, tant qu'une de ces méthodes est toujours activée. Dans les prochaines semaines, les utilisateurs sans A2F seront retirés de ces groupes. Cela s'applique entre autres aux bureaucrates. Veuillez lire les tâches liées pour les dates de déploiement. [https://phabricator.wikimedia.org/T423119][https://phabricator.wikimedia.org/T423120] * L'[[m:Special:MyLanguage/WMDE Technical Wishes|équipe des souhaits techniques de Wikimédia Allemagne (WMDE)]] va lancer un [[w:fr:Test A/B|test A/B]] sur [[:phab:T415904|10 wikis]], pour essayer des [[m:WMDE Technical Wishes/References/Reference Previews|améliorations potentielles pour les aperçus de références]]. Cette expérience durera environ 2 semaines à la fin mai ou début juin et affectera 10 % du lectorat sur ordinateur sur les wikis participants. * Après deux expériences fructueuses, l'équipe Croissance du lectorat déploiera une fonctionnalité de [[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|visionnage d'images]] en bêta pour toutes les Wikipédia sur mobile le 25 mai. Cela veut dire que toutes les personnes ayant les fonctionnalités bêtas activées verront cette fonctionnalité. Les autres pourront l’activer dans leurs préférences. Cette fonctionnalité inclura un carrousel de toutes les images d'un article en haut de celui-ci, avec la possibilité pour les contributeurs d’[[mw:Readers/Reader_Growth/Image_Browsing#Phase_2.1_beta_feature|exclure des images du carrousel d'un article ou d'enlever la fonctionnalité pour l'entièreté de l'article]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, les fichiers STL tridimensionnels étaient affichés incorrectement par l'extension 3D du lecteur multimédia, ce qui est maintenant corrigé. [https://phabricator.wikimedia.org/T416723] '''Actualités pour la contribution technique''' * Les classes CSS dépréciées <bdi lang="zxx" dir="ltr"><code><nowiki>tleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>tright</nowiki></code></bdi> ont été remplacées par <bdi lang="zxx" dir="ltr"><code><nowiki>floatleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>floatright</nowiki></code></bdi> car les premières ne fonctionnent pas correctement sur toutes les plateformes, dont l'interface web mobile et l'application mobile. Les projets se servant de ces classes sont encouragés à vérifier leur usage et à planifier leur migration. Sachez que <bdi lang="zxx" dir="ltr"><code><nowiki>floatleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>floatright</nowiki></code></bdi> pourraient aussi être dépréciées dans le futur, même s’il n'y a pas de calendrier défini. [[phab:T426452|En savoir plus]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.4|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/22|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W22"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 25 mai 2026 à 23:52 (CEST) <!-- Message envoyé par User:Quiddity (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30584502 --> == Votez maintenant aux élections 2026 de l'U4C == <section begin="announcement-content" /> Les votants éligibles sont invités à participer à l'élection 2026 du [[m:Special:MyLanguage/Universal_Code_of_Conduct/Coordinating_Committee|Comité de coordination du Code de conduite universel]]. De plus amples informations – notamment sur la vérification de l'éligibilité, le processus de vote, les candidats et un lien vers le scrutin – sont disponibles sur Meta à la [[m:Special:MyLanguage/Universal_Code_of_Conduct/Coordinating_Committee/Election/2026|page d'informations sur les élections de 2026]]. Le scrutin se termine le 2 juin 2026 à [https://zonestamp.toolforge.org/1780358400 00 h 00 UTC]. Veuillez voter si votre compte est éligble. Les résultats seront disponibles avant le 14 juin 2026. -- en coopération avec l'U4C.<section end="announcement-content" /> [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|talk]]) 27 mai 2026 à 19:14 (CEST) <!-- Message envoyé par User:Keegan (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == Actualités techniques n° 2026-23 == <section begin="technews-2026-W23"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/23|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L'équipe [[mw:Special:MyLanguage/Readers/Reader Experience|Reader Experience]] mène une expérience pour montrer la fonctionnalité [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|listes de lecture]], qui est encore en développement, aux lecteurs non connectés sur mobile afin de tester si elle encourage la création de compte à un rythme plus élevé que le bouton watchstar. L'[[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists#Experiment timeline|expérience]] a été lancée le 18 mai sur les wikis en allemand, espagnol, italien, portugais, polonais, néerlandais, turc et ourdou, et elle durera un mois. * L'équipe Wikimedia Apps a publié la [[mw:Special:MyLanguage/Wikimedia Apps/Team/Explore Feed Refresh/Phase 1|Phase 1]] du flux d'accueil repensé pour l'application Android Beta. Le nouveau flux d'accueil comprend un onglet « Communauté » actualisé et un onglet « Pour vous » personnalisé contenant des recommandations de lecture mises à jour quotidiennement. La refonte fait partie d'un effort plus large visant à améliorer la découverte de contenu et à créer des expériences d'apprentissage plus engageantes dans les applications Wikipédia. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:18|la tâche soumise|les {{formatnum:18}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:18||s}} la semaine dernière]]. Par exemple, un problème où les images pouvaient ne pas se charger pour certaines modifications suggérées sur [[w:Special:Homepage|Special:Homepage]], laissant la vignette bloquée dans un état de chargement, a maintenant été corrigé. [https://phabricator.wikimedia.org/T424048] '''Actualités pour la contribution technique''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.5|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/23|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W23"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 1 juin 2026 à 23:08 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30613639 --> == Actualités techniques n° 2026-24 == <section begin="technews-2026-W24"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/24|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Wikimedia Entreprise a relevé les limites d’utilisation gratuite de ses API. La limite mensuelle de requêtes pour l’API « à la demande » (<i lang="en">On-demand</i>) est passée de {{formatnum:5000}} à {{formatnum:50000}} requêtes, tandis que celle de l’API des instantanés (<i lang="en">Snapshot</i>) est passée de 15 à 30 requêtes par mois. De plus, les instantanés de contenus structurés sont désormais accessibles aux comptes gratuits. Ces changements élargissent l’accès aux données de Wikimedia Entreprise pour les développeurs et développeuses, les chercheurs et chercheuses et les organisations qui utilisent les contenus Wikimédia. [https://enterprise.wikimedia.com/blog/enhanced-free-api] '''Actualités pour la contribution''' * La [[mw:Special:MyLanguage/Wikimedia_Apps/Team/Explore Feed Refresh/Phase 1|nouvelle version du Fil d’exploration]], désormais appelé « Fil d’accueil », est en cours de déploiement auprès de 50 % des utilisateurs de l’application Wikipédia pour Android. Le fil d’accueil aide le lectorat à découvrir du contenu pertinent grâce à deux nouveaux onglets : « Communauté » et « Pour vous ». L’onglet « Communauté » propose un flux défilant de contenus sélectionnés et d’actualités provenant de l’ensemble de la communauté et du mouvement Wikimédia, tandis que l’onglet « Pour vous » offre une expérience en plein écran et par glissement qui présente des contenus adaptés aux centres d’intérêt de l’utilisateur ou utilisatrice. Cette refonte s’inscrit dans le cadre d’un travail en cours visant à améliorer la découverte et à enrichir l’expérience d’apprentissage au sein de l’application Wikipédia. * Le jeu-questionnaire quotidien [[mw:Special:MyLanguage/Wikimedia Apps/Team/iOS/"Which came first?" Game|Qu’est-ce qui est arrivé en premier ?]] est désormais disponible dans la version bêta de l’application Wikipédia pour iOS en anglais, allemand, français, portugais, russe, espagnol, arabe, chinois et turc. Le jeu s’appuie sur des événements historiques tirés de la rubrique « Éphéméride » de Wikipédia et met les lecteurs au défi de deviner lequel des deux événements s’est produit en premier. Le jeu avait déjà été lancé sur Android. Les communautés souhaitant rendre le jeu disponible dans leur langue peuvent [[mw:Special:MyLanguage/Wikimedia_Apps/Team/Games#Game availability by language|consulter les instructions et les conditions requises]]. * [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|Les sous-références]], une nouvelle fonctionnalité de MediaWiki permettant aux contributeurs de réutiliser des références avec des détails différents, va commencer à être déployée sur les wikis Wikimédia après une phase pilote réussie. Le déploiement débutera le 8 juin pour la plupart des [[wikitech:Deployments/Train#Wednesday|wikis du groupe 1]] et Wikipédia en français, puis d'autres éditions linguistiques de Wikipédia bénéficieront de cette fonctionnalité au cours des prochains mois. Les communautés sont invitées à se préparer en vérifiant s’il existe des [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-cite&language=en&action_source=search&filter=%21translated&optional=1&action=translate messages non traduits de l’extension Cite] dans leur langue et en passant en revue toute utilisation de l’outil [[mw:Special:MyLanguage/Reference Tooltips|Infobulles des références]], qui pourraient nécessiter des [[:phab:T416304#11668731|mises à jour]] pour prendre en charge la nouvelle fonctionnalité. Les wikis utilisant les [[mw:Special:MyLanguage/Help:Reference Previews|aperçus de référence]] n’ont aucune action à entreprendre. Les communautés peuvent également créer la [[Special:TrackingCategories|catégorie de suivi]] ''cite-tracking-category-ref-details'' en tant que catégorie cachée à l’aide de <code><nowiki>__HIDDENCAT__</nowiki></code> (ou d’un modèle dédié), et la relier à l’élément Wikidata correspondant [[d:Q129764848]]. [https://phabricator.wikimedia.org/T425662] * L'[[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews#Experimentation|expérience d'Aperçus de page]] sur le Web mobile a pris fin. L'équipe a décidé de ne pas déployer cette fonctionnalité après que les résultats ont montré qu'elle n'avait pas d'impact statistiquement significatif sur la fidélisation des lecteurs, l'amélioration de la fidélisation étant le principal indicateur de réussite. Les « Aperçus de page », déjà disponibles sur ordinateur et dans les applications, affichent une vignette, le premier paragraphe et un lien vers l'article complet lorsque les lecteurs cliquent sur un lien bleu. L'expérience a testé cette fonctionnalité sur le Web mobile sur six versions de Wikipédia. * La [[mw:Special:MyLanguage/Codex/Design/Icons|bibliothèque d'icônes de l'interface utilisateur]] sera [[phab:T399175|mise à jour dans le courant de cette semaine ou la semaine prochaine]]. La plupart des quelque 300 icônes ont été légèrement peaufinées et une trentaine de nouvelles icônes ont été ajoutées. Ces modifications améliorent les icônes afin de les rendre plus cohérentes et plus compréhensibles, et d'offrir un meilleur équilibre visuel lorsqu'elles sont utilisées en groupe. * L'interface [[mw:Special:MyLanguage/Universal Language Selector|Sélecteur universel de langue]] (ULS) de MediaWiki, qui aide les utilisateurs à sélectionner du contenu dans d'autres langues, a été mise à jour. La nouvelle version améliore la rapidité et l'accessibilité, et les utilisateurs des projets Wikimédia peuvent désormais épingler des langues pour changer de langue plus rapidement. Le déploiement sur les sites Wikimédia se fera progressivement au cours des prochaines semaines. Vous pouvez la tester dès maintenant en tant que fonctionnalité bêta en sélectionnant [[Special:Preferences#mw-prefsection-betafeatures|les fonctionnalités bêta]] dans les préférences de votre profil et partager vos commentaires sur [[mw:Special:MyLanguage/Universal Language Selector/New ULS|la page du projet]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:21|la tâche soumise|les {{formatnum:21}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:21||s}} la semaine dernière]]. Par exemple, un problème du le tableau de bord d'analyse des pages vues sur pageviews.wmcloud.org qui a arrêté de mettre à jour les données graphiques en mai 2026, affectant tous les utilisateurs, a été résolu. [https://phabricator.wikimedia.org/T427171] '''Actualités pour la contribution technique''' * La signature de la fonction <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink()</nowiki></code></bdi> a été simplifiée. Les développeurs peuvent désormais passer un objet de configuration à la place d'une liste de paramètres positionnels lors de la création de liens vers des portlets. L'ancienne signature de la fonction reste prise en charge à des fins de compatibilité ascendante. Par exemple, au lieu de : <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink('p-cactions', '#', 'Stub', 'ca-stubtag', 'Add a stub tag to this page');</nowiki></code></bdi>, utilisez <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink('p-cactions', { href: '#', text: 'Stub', id: 'ca-stubtag', tooltip: 'Add a stub tag to this page' });</nowiki></code></bdi>. Les responsables de la maintenance des scripts sont invités à passer en revue les utilisations existantes de <bdi lang="zxx" dir="ltr"><code><nowiki>addPortletLink()</nowiki></code></bdi> et à les mettre à jour si nécessaire. Cette modification sera disponible sur tous les wikis à partir du 11 juin. Merci à Gerges, bénévole de la communauté, d'avoir apporté cette amélioration. [https://phabricator.wikimedia.org/T427945] * '''Discussion sur la liste de souhaits de la communauté''': les [[m:Special:MyLanguage/Community Wishlist/Updates#May 20, 2026: Community Tech becomes a program|changements introduits]] par les équipes Produit et Technologie visent à augmenter le nombre et la complexité des souhaits exaucés, notamment par la dissolution de l'équipe Community Tech. Ils [[m:Special:MyLanguage/Community Wishlist/Updates|mènent actuellement des discussions]] sur une [[m:Talk:Community Wishlist#Proposed direction for Wishlist|orientation proposée pour la liste de souhaits]] émanant des membres de la communauté. Cela inclut des moyens de structurer le vote annuel, un meilleur suivi des souhaits, la suppression de certains domaines prioritaires et des [[m:Special:MyLanguage/Community Wishlist/Updates|mises à jour concernant le personnel]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.6|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/24|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W24"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 8 juin 2026 à 23:29 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30650573 --> == Actualités techniques n° 2026-25 == <section begin="technews-2026-W25"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/25|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L'[[mw:Special:MyLanguage/Readers/Reader Growth|équipe chargée de la croissance du lectorat]] a lancé une fonctionnalité bêta d'[[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|exploration des images]] sur la version mobile de toutes les Wikipédias. Cette fonctionnalité affiche un carrousel d'images en haut des articles contenant au moins trois images. Les contributeurs peuvent configurer cette fonctionnalité à l'aide des commandes suivantes : pour masquer une image spécifique sur une page, utilisez soit <code>class=notpageimage</code> pour l'exclure des aperçus miniatures, soit <code>class=noviewer</code> pour l'exclure de MediaViewer. Le carrousel peut également être désactivé complètement sur une page à l'aide du mot magique <code><nowiki>__NOMEDIAVIEWERCAROUSEL__</nowiki></code>. Pour faire des retours ou signaler des bugs, rendez-vous sur la [[mw:Talk:Readers/Reader Growth/Image Browsing|page de discussion du projet]]. * Les [[mw:Special:MyLanguage/Help:Tables#class="wikitable"|Wikitables]] peuvent désormais être [[mw:Special:MyLanguage/Help:Sortable tables#Forcing the initial sort direction|triées par ordre décroissant]] dès le premier clic en ajoutant <code dir=ltr>data-sort-order="desc"</code> à la cellule d'en-tête. Auparavant, par défaut, cliquer une première fois sur l'en-tête d'une colonne entraînait un tri par ordre croissant. Cette nouveauté offre davantage de contrôle et de flexibilité pour les Wikitables, tandis que le comportement par défaut pour les clics suivants reste inchangé. [https://phabricator.wikimedia.org/T398416] '''Actualités pour la contribution''' * La fonctionnalité d'[[mw:Special:MyLanguage/Article guidance|Aide à la rédaction d'articles]] est actuellement en phase de test auprès de certains contributeurs qui créent de nouveaux articles sur les Wikipédias en anglais simplifié, en français et en turc. L'expérience débutera bientôt sur les Wikipédias en arabe et en bengali également. [[w:simple:Special:NewArticle|Cette fonctionnalité]] fournit aux contributeurs des conseils élaborés par la communauté afin de les aider à créer des articles conformes aux normes communautaires. Les contributeurs expérimentés peuvent continuer à créer ou à adapter des modèles pour des types d'articles spécifiques qui sont couramment créés par des contributeurs moins expérimentés. Ces modèles guident les contributeurs moins expérimentés dans la création d'articles de haute qualité. Un guide rapide des balises utilisées dans les modèles est disponible sur [[mw:Special:MyLanguage/Article guidance/Test feature guide#Markups in outlines|cette page]]. [[w:fr:Projet:Aide à la rédaction d'articles#Liste de plans d'aide à la rédaction|Des exemples de modèles]] pouvant être adaptés, ainsi que des instructions sur la manière de les adapter, se trouvent dans [[mw:Special:MyLanguage/Article guidance#Adapting a sample outline in a Wikipedia|cette section]] de la page du projet. * Les wikis qui souhaitent remplacer le bouton « indéfiniment » dans la page Special:Block pour les comptes temporaires (par exemple, les wikis qui bloquent les utilisateurs temporaires uniquement jusqu'à l'expiration de leur compte) pourront le faire en créant [[MediaWiki:ipb-indefinite-expiry-temporary-account]] avec la durée de blocage souhaitée. [https://phabricator.wikimedia.org/T427125] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:41|la tâche soumise|les {{formatnum:41}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:41||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * D'ici la fin du mois de juin, une chaîne « user-agent » valide sera requise pour les téléchargements automatisés de sauvegardes depuis le site dumps.wikimedia.org. Les requêtes automatisées fournissant une chaîne « user-agent » générique ou vide seront bloquées. Cette mesure [[phab:T400119|renforce l'application]] de la [[foundation:Special:MyLanguage/Policy:Wikimedia Foundation User-Agent Policy|politique relative à l'agent utilisateur]] en vigueur depuis longtemps. L'accès aux sauvegardes via Wikimedia Cloud Services restera inchangé. * La mise en place des [[mw:Wikimedia APIs/Rate limits|limites de débit des API]] à l'échelle mondiale est désormais achevée ; ces limites s'appliquent à toutes les API et sont fixées aux niveaux indiqués dans la documentation pour tous les groupes. Les bots fonctionnant sur Toolforge/WMCS ou disposant du droit d'utilisateur « bot » sur n'importe quel wiki restent exemptés. Tous les bots doivent continuer à respecter les bonnes pratiques décrites dans la documentation afin d'éviter d'être soumis à des limites de débit. * Le [https://api.wikimedia.org/wiki/Main_Page wiki du portail API] sera en lecture seule à partir de cette semaine (du 15 au 18 juin). La semaine suivante (du 22 au 25 juin), toutes les URL du wiki du portail API redirigeront vers [[mw:Wikimedia APIs|les API Wikimedia sur mediawiki.org]]. Pour en savoir plus, consultez la [[wikitech:API Portal/Deprecation|page du projet]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.7|MediaWiki]] '''Rencontres et évènements''' * Le 17 juin à 18 h (UTC), la WMF organisera une réunion sur Discord consacrée à la revue de code. L'[[mw:Special:MyLanguage/Developer Satisfaction Survey/2026|enquête sur la satisfaction des développeurs]] nous a permis de constater que les bénévoles rencontrent des difficultés avec la revue de code, et nous souhaitons discuter de ces expériences afin de trouver des solutions concrètes. Vous pouvez rejoindre la réunion [https://discord.gg/wikipedia?event=1514727511102062664 via le serveur Discord de la communauté Wikimedia]. * La [[m:Special:MyLanguage/Conferencia Wikimedia de América Latina 2026|Conférence Wikimedia d'Amérique latine]] organisera un hackathon régional qui réunira la communauté technique du mouvement Wikimedia, notamment des développeurs, des administrateurs système, des data scientists et des utilisateurs disposant de droits étendus. Les contributeurs techniques intéressés peuvent [https://docs.google.com/forms/d/e/1FAIpQLSf4osJzTHBJjQbYJk7TMVEJjTEQv7IgtsUDfP-o-qTgeRQQxw/viewform postuler à une bourse] pour y participer jusqu'au 21 juin à minuit (heure de la Bolivie, UTC-4). * Inscrivez-vous aux Wikimania Team Challenges pour participer à cet événement exceptionnel. Les défis par équipe se dérouleront en ligne et en présentiel les 21 et 22 juillet, avant la conférence Wikimania. Tout le monde est le bienvenu, quelles que soient ses compétences ou son inscription à Wikimania. Les équipes travailleront sur 10 défis importants visant à soutenir la communauté Wikimedia. Pour plus de détails, rendez-vous sur [[wmania:Special:MyLanguage/2026:Team challenges|la page des défis par équipe]] et [https://wikimedia.eventyay.com/wm/teamchallenges/ inscrivez-vous ici]. Les inscriptions se terminent le 20 juin à 23 h UTC. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/25|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W25"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 15 juin 2026 à 18:48 (CEST) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30689604 --> == Actualités techniques n° 2026-26 == <section begin="technews-2026-W26"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/26|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Les [[mw:Special:MyLanguage/Growth/Feature summary|fonctionnalités de croissance]] sont [[phab:T418115|désormais disponibles sur Wikidata]]. Cette mise à jour permet d'accéder au mentorat ([[mw:Special:MyLanguage/Help:Growth/Mentorship|s'il est configuré]]), au module Impact, au panneau d'aide et à une page d'accueil simplifiée pour les nouveaux arrivants (sans les suggestions de modifications). Les administrateurs de Wikidata continuent de paramétrer ces fonctionnalités via la configuration communautaire. '''Actualités pour la contribution''' * La page spéciale [[{{#special:RangeCalculator}}]] a été créée. Elle permet aux utilisateurs de trouver une plage d'adresses IP sans avoir à recourir à des outils externes. Jusqu'à présent, cet outil n'était accessible qu'aux CheckUsers. [https://phabricator.wikimedia.org/T268429] * Les [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|sous-références]] sont une nouvelle fonctionnalité de MediaWiki qui permet aux contributeurs de réutiliser des références en modifiant certains détails. Elle sera déployée le 23 juin, sur la plupart des versions de Wikipédia de petite et moyenne taille. La [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#deployment|FAQ]] répertorie les mesures à prendre sur votre wiki pour faciliter ce déploiement. Consultez le [[:phab:T414094|plan de déploiement]] pour connaître les prochaines étapes. [https://phabricator.wikimedia.org/T428902] * À partir de la semaine prochaine, les utilisateurs recevront une notification lorsqu'ils seront bloqués ou débloqués pour l'édition, ou si ce blocage venait à changer. [https://phabricator.wikimedia.org/T100974] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:32|la tâche soumise|les {{formatnum:32}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:32||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * À partir de la semaine prochaine, les filtres anti-abus configurés pour « exiger une vérification par CAPTCHA » s'appliqueront également aux utilisateurs disposant du droit <code>skipcaptcha</code>, ce qui inclut la plupart des utilisateurs auto-confirmés. Les bots en sont exemptés. Ce changement ne concerne que les modifications qui déclenchent un filtre anti-abus. Le droit <code>skipcaptcha</code> continuera à exempter les utilisateurs de l'obligation de résoudre des CAPTCHA dans le cadre d'une utilisation normale des wikis. [https://phabricator.wikimedia.org/T402595] * La documentation de référence relative à l'[[wikitech:Machine_Learning/LiftWing/API|API Lift Wing]] a été déplacée du portail API vers le [https://wikitech.wikimedia.org/w/index.php?api=lift-wing&title=Special%3ARestSandbox bac à sable REST] interactif. * Le wiki du Portail API est désormais fermé. Pour consulter la documentation relative aux API, rendez-vous sur [[mw:Special:MyLanguage/Wikimedia_APIs|Wikimedia APIs sur mediawiki.org]]. À compter du 22 juin, toutes les URL du wiki du Portail API (https://api.wikimedia.org/wiki/) redirigeront vers la page de mediawiki.org. [https://phabricator.wikimedia.org/T427537] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.8|MediaWiki]] '''Rencontres et évènements''' * Participez à une visioconférence le 25 juin à 14 h 30 UTC pour rencontrer les stagiaires actuels de Wikimédia participant au [[mw:Google_Summer_of_Code/2026|Google Summer of Code]] et à [[mw:Outreachy/Round_32|Outreachy]]. Les stagiaires présenteront leurs projets et feront une brève démonstration du travail qu'ils ont réalisé jusqu'à présent. Les participants sont invités à [[mw:event:Google_Summer_of_Code/Summer_2026_June_Internship_open_session|partager leurs idées et leurs contacts au sein de leur communauté]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/26|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W26"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 juin 2026 à 15:05 (CEST) <!-- Message envoyé par User:Trizek (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30722494 --> == RFC about AI-generated content in Wikimedia Commons == <bdi lang="en" dir="ltr">Apologies for writing in English, please help translate this message to your language. You are invited to participate in a [[c:Commons:Requests for comment/Policy update for AI content|request for comment on Wikimedia Commons about a policy update for AI content]]. This may affect files that are uploaded to Wikimedia Commons for use on this project. Thank you. [[m:User:Codename Noreste|Codename Noreste]] ([[m:User talk:Codename Noreste|discussion]])</bdi> 23 juin 2026 à 19:11 (CEST) <!-- Message envoyé par User:Codename Noreste@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == Intégration du lien vers les contacts juridiques et de sécurité dans le pied de page de votre wiki == <section begin="Message"/> '''Contacts juridiques et de sécurité''' Bonjour à toute la communauté, la Fondation Wikimedia a mis à disposition une [[wmf:Special:MyLanguage/Legal:Wikimedia Foundation Legal and Safety Contact Information|page unique dédiée aux mentions légales et à la sécurité]], à ajouter en pied de page de votre wiki, afin de garantir l'accès à des informations juridiques exactes. Il s'agit d'une exigence réglementaire. Nous avons déjà mis en place des liens vers les wikis en anglais, allemand, italien, espagnol et d'autres langues de Wikipedia, et nous les déploierons bientôt sur votre wiki. Pour en savoir plus, [[m:Special:MyLanguage/Wikimedia_Foundation_Legal_and_Safety_Contacts_FAQ|consultez la page du projet]] et n'hésitez pas à laisser vos commentaires dans ce fil de discussion ou sur la [[m:Special:MyLanguage/Talk:Wikimedia Foundation Legal and Safety Contacts FAQ|page de discussion]]. <section end="Message"/> -- [[User:Sannita (WMF)|User:Sannita (WMF)]] ([[User talk:Sannita (WMF)|talk]]) 25 juin 2026 à 15:30 (CEST) <!-- Message envoyé par User:Sannita (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Sannita_(WMF)/Mass_sending_test&oldid=30731267 --> == Actualités techniques n° 2026-27 == <section begin="technews-2026-W27"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/27|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Dans le cadre des [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|expériences sur la création de comptes]], l'équipe Growth a testé l'ajout d'une icône de compte utilisateur dans l'en-tête du site mobile pour les utilisateurs non connectés, offrant ainsi un accès direct aux actions « Créer un compte » et « Se connecter ». Cette expérience a permis d'augmenter le nombre de créations de comptes d'environ 20 % sans nuire à la qualité des modifications ni au taux de modifications constructives. Cette fonctionnalité sera désormais déployée sur tous les wikis de la Fondation Wikimédia sur le site mobile au cours de la première semaine de juillet. [https://phabricator.wikimedia.org/T428220] * À la suite d'une [[phab:T426248|expérience concluante]], les utilisateurs connectés qui n'ont pas [[mw:Special:MyLanguage/Help:Email_confirmation|confirmé leur adresse e-mail]] lors de la création de leur compte voient s'afficher une nouvelle bannière leur demandant de finaliser cette procédure. Cela permet de réduire le risque que les utilisateurs se retrouvent bloqués hors de leur compte et rend les adresses e-mail associées aux comptes globalement plus fiables. Cette mesure s'inscrit dans le cadre du projet [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security|Sécurité des comptes]]. [https://phabricator.wikimedia.org/T428292] * Une mise à jour de [[Special:Search|Recherche]] affine le comportement de <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> lorsqu'il est utilisé pour exclure des résultats. Auparavant, l'utilisation de <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> avec la négation pouvait élargir involontairement les résultats de recherche en ajoutant les espaces de noms inclus dans le champ de recherche, ce qui entraînait un comportement déroutant pour les utilisateurs s'attendant à un filtre d'exclusion simple. Avec cette mise à jour, <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> exclura désormais strictement les titres de pages correspondants comme prévu et pourra afficher un avertissement si l'espace de noms concerné n'a pas été explicitement sélectionné. Le comportement de <bdi lang="zxx" dir="ltr"><code><nowiki>prefix:</nowiki></code></bdi> sans négation reste toutefois inchangé. [https://phabricator.wikimedia.org/T427443] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:33|la tâche soumise|les {{formatnum:33}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:33||s}} la semaine dernière]]. Par exemple, le problème qui empêchait les réviseurs utilisant la barre d'outils « Page Curation » d'être automatiquement abonnés aux discussions qu'ils avaient lancées sur les pages de discussion a désormais été résolu. Les réviseurs recevront désormais des notifications lorsqu'une personne répondra à ces discussions. [https://phabricator.wikimedia.org/T329346] '''Actualités pour la contribution technique''' * À compter du 29 juin, les téléchargements automatisés depuis le site web « dumps.wikimedia.org » seront soumis à la [[Foundation:Special:MyLanguage/Policy:Wikimedia Foundation User-Agent Policy|politique relative aux user-agents]]. Les requêtes automatisées utilisant un user-agent générique ou vide seront bloquées. L'accès aux sauvegardes via Wikimedia Cloud Services n'est pas affecté. Cette mesure fait suite à l'annonce publiée dans le [[m:Special:MyLanguage/Tech/News/2026/25|numéro 2026/25 de Tech News]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.9|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/27|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W27"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 29 juin 2026 à 13:48 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30744833 --> == Actualités techniques n° 2026-28 == <section begin="technews-2026-W28"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/28|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:34|la tâche soumise|les {{formatnum:34}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:34||s}} la semaine dernière]]. Par exemple, les résultats de la barre de recherche sur Wikidata étaient en anglais au lieu d’utiliser la bonne langue de repli pour les utilisateurs de variantes de langues. Ce problème a maintenant été corrigé : les suggestions de recherche suivront désormais correctement la chaîne des langues de repli. [https://phabricator.wikimedia.org/T429769] '''Actualités pour la contribution technique''' * En préparation de la [[m:Special:MyLanguage/Event:Celebrate Women|campagne Célébrons les femmes]] prévues pour mars 2027, l’[[m:Special:MyLanguage/Wikimedia Foundation/Advancement/Community Growth/Content Enablement|équipe Activation du contenu]] de Wikimedia Foundation a lancé un sondage composé de 22 questions pour mieux comprendre les contributions techniques des personnes s’identifiant comme femmes sur les projets Wikimedia. Répondre au sondage prend environ 15 à 20 minutes ; il restera ouvert jusqu’au 20 juillet 2026. Les [[m:Special:MyLanguage/Celebrate Women/Technical contributions survey|questions]] sont aussi sur wiki pour les regarder auparavant. * L’[[mw:Special:MyLanguage/Extension:Score|extension Partitions]] prend désormais en charge le rendu de partition musicales comme images SVG, en plus du format PNG. Cela répond à [[:phab:T49578|une vieille demande]] et résout des problèmes anciens de qualité de l’image. Les deux formats sont désormais fournis aux clients web : PNG dans l’attribut <bdi lang="zxx" dir="ltr"><code><nowiki>src</nowiki></code></bdi> et SVG dans l’attribut <bdi lang="zxx" dir="ltr"><code><nowiki>srcset</nowiki></code></bdi>. * Le nouvel analyseur syntaxique [[wikitech:Parsoid|Parsoid]] continue [[mw:Special:MyLanguage/Parsoid/Parser_Unification/Updates|d’être déployé sur d’autres wikis]], ce qui rend plus facile l’introduction de nouvelles fonctionnalités de lecture et de modification. Il a été activé sur Wikipédia en français, amenant la couverture totale à 78,9 % des pages vues de Wikipédia. Le déploiement sur Wikipédia en anglais sur ordinateur va progresser cette semaine. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.10|MediaWiki]] '''En détails''' * La [[diffblog:2026/06/29/wikimedia-hackathon-2026-building-collaborating-and-shaping-the-future-together/|publication sur le blog récapitulant]] le Hackathon Wikimedia 2026 est désormais en ligne. Il met en avant les projets, séances et activités sociales de l’évènement de cette année et présente les premières grandes lignes du Hackathon Wikimedia 2027. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/28|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W28"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 6 juillet 2026 à 15:56 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30773578 --> == Actualités techniques n° 2026-29 == <section begin="technews-2026-W29"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/29|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[mw:Special:MyLanguage/Growth/Revise_Tone|Revise Tone]] aide les nouveaux contributeurs à identifier les passages des articles de Wikipédia susceptibles de contenir un langage non encyclopédique et les encourage à envisager d'en réviser le ton. Cette fonctionnalité a fait l'objet d'un [[w:en:A/B_testing|test A/B]] sur les Wikipédias en arabe, en anglais, en français et en portugais, où le taux d'achèvement des tâches par les nouveaux contributeurs [[mw:Special:MyLanguage/Growth/Revise_Tone#Experiment_Results|a augmenté de 38,7 %]] par rapport à la tâche de relecture par défaut, sans que la qualité des modifications n'en soit affectée. Le test s'est achevé le 9 juillet et la fonctionnalité est désormais accessible à tous sur ces wikis ; elle est configurable via la configuration communautaire. [[phab:T426364|Il est prévu]] de déployer « Revise Tone » sur d’autres wikis. * La configuration communautaire permettant la [[mw:Special:MyLanguage/Help:Growth/Mentorship#Automated mentor list cleanup|suppression automatique des mentors inactifs]] selon des critères configurables sera activée le jeudi 16, [[mw:Special:MyLanguage/Growth/Deployment|sur certains wikis]], afin de maintenir à jour les listes de mentors. Les mentors sont des contributeurs expérimentés qui choisissent d'aider les nouveaux utilisateurs sur le wiki grâce aux [[mw:Special:MyLanguage/Growth/Feature summary|fonctionnalités de croissance]]. Les administrateurs peuvent désormais préparer les paramètres via [[w:Special:CommunityConfiguration/Mentorship|Special:CommunityConfiguration/Mentorship]] ; ces fonctionnalités prendront effet à partir de jeudi. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:38|la tâche soumise|les {{formatnum:38}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:38||s}} la semaine dernière]]. Par exemple, un problème où certains utilisateurs de l'application Android Wikipedia ont été déconnectés immédiatement après leur connexion, les empêchant de rester connectés et de modifier les pages, a maintenant été résolu. [https://phabricator.wikimedia.org/T316916] '''Actualités pour la contribution technique''' * La modification d'une page à l'aide de scripts utilisateur ou de gadgets entraînait la réinitialisation des étiquettes de liste de suivi que l'utilisateur avait attribuées à cette page. Ce problème a désormais été résolu. [https://phabricator.wikimedia.org/T423778] * Pour contourner un bug de Safari (voir [[phab:T425211]]), sur les wikis utilisant Parsoid, wikilink hrefs utilise désormais des URL absolues au lieu d'URL relatives au protocole. La sortie de l'API REST reste inchangée et continue d'utiliser des URL relatives au protocole. Les gadgets, les scripts utilisateur, les bots et les feuilles de style CSS devront probablement être adaptés s'ils s'appuyaient sur la présence d'URL relatives au protocole dans wikilink hrefs. [https://phabricator.wikimedia.org/T431358] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.11|MediaWiki]] '''En détails''' * L'équipe chargée de la plateforme d'expérimentation de la Fondation Wikimedia a publié un article de blog faisant le bilan de sa première année d'expérimentation structurée. Cet article met en avant des expériences couronnées de succès, telles que « Paste Check », « Reference Check » et « Tone Check », qui ont permis d'améliorer les résultats des modifications et ont été étendues à un plus grand nombre d'utilisateurs, ainsi que des expériences qui n'ont pas abouti à des changements au niveau du produit. [[diffblog:2026/07/07/moving-the-needle-how-we-test-new-ideas-across-wikimedia-projects|En savoir plus]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/29|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W29"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 13 juillet 2026 à 18:11 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30804401 --> == Actualités techniques n° 2026-30 == <section begin="technews-2026-W30"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/30|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L'[[mw:Special:MyLanguage/Reader/Reader Experience|équipe chargée de l'expérience utilisateur]] a pris en compte les commentaires de la communauté concernant l'emplacement des boutons « Watchstar » et « Watchlist » pour la [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|fonctionnalité bêta « Listes de lecture »]], qui permettrait d'enregistrer des articles pour les lire plus tard – un [[m:Special:MyLanguage/Community Wishlist/W102|élément de la liste de souhaits]] visant à intégrer cette fonctionnalité sur le site web. Les contributeurs sont invités à activer cette fonctionnalité bêta pour la tester et [[phab:T426453|partager leurs impressions]]. * Le [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|mode Suggestions]] propose des suggestions d'édition au sein de l'éditeur visuel afin d'améliorer les articles de Wikipédia. [[Special:EditChecks|Toutes les suggestions]] sont [[mw:Special:MyLanguage/Help:Suggestion mode#For administrators – local customization|configurables par la communauté]]. La fonctionnalité [[mw:Special:MyLanguage/Edit check/TextMatch|TextMatch]] permet aux bénévoles de créer des suggestions locales personnalisées. Cette fonctionnalité recherche des chaînes de texte dans les articles et prend désormais en charge les expressions régulières. Cela offre aux bénévoles davantage de précision et de flexibilité quant aux types de suggestions locales qu’ils peuvent créer. Remarque : les suggestions [[mw:Special:MyLanguage/Edit check/Configuration#:~:text=maximumEditcount|peuvent être ciblées]] en fonction du nombre de modifications effectuées par la personne qui édite, ainsi que d’autres aspects de la page. Vous pouvez [[mw:Special:MyLanguage/Help:Suggestion mode#Create custom local types of Suggestions|trouver des exemples provenant d’autres communautés]] pour vous inspirer, notamment des TextMatches qui détectent : les fautes de frappe, les erreurs grammaticales, les publicités potentielles, les clichés, l’utilisation incorrecte des tirets ou des traits d’union, les mots-clés temporels non spécifiques, les noms obsolètes, et bien plus encore. Tout [[mw:Special:MyLanguage/Talk:VisualEditor/Suggestion Mode|retour]] est le bienvenu. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, un problème lié à l'outil SVG Translate, qui pouvait utiliser une version obsolète d'un fichier, entraînant ainsi le remplacement des traductions existantes lors du téléchargement de nouvelles traductions, a désormais [[phab:T430577|été résolu]]. Au total, au cours du dernier trimestre, d'avril à juin 2026, environ 337 tâches communautaires ont été résolues par la Fondation Wikimedia. '''Actualités pour la contribution technique''' * Sur les wikis utilisant Parsoid, ce dernier affiche désormais un maximum de 1 250 images par page. Une nouvelle catégorie de suivi, « media-limit-reached », sera bientôt disponible pour identifier les pages où cette limite est atteinte, ce qui facilitera la recherche des contenus dont l'affichage des médias a pu être restreint lors du rendu. Consultez [[phab:T430854]] pour plus d'informations et pour nous faire part de vos commentaires. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.12|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/30|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W30"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 21 juillet 2026 à 07:46 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30836091 --> == Request for comment (the future of Abstract Wikipedia) == <bdi lang="en" dir="ltr" class="mw-content-ltr"> Apologies if this has not yet been translated into your wiki's language. {{int:Please-translate}} You are invited to voice your opinions in a [[:m:Requests for comment/The future of Abstract Wikipedia|request for comment about the future of Abstract Wikipedia]]. {{Int:Feedback-thanks-title}} [[:m:User:Kowal2701|Kowal2701]] ([[:m:User talk:Kowal2701|talk]]) 24 juillet 2026 à 14:24 (CEST) </bdi> <!-- Message envoyé par User:DreamRimmer@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == <span lang="en" dir="ltr">Tech News: 2026-31</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W31"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/31|Translations]] are available. '''Updates for editors''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Wishlist item]] [[mw:Special:MyLanguage/ContentTranslation|Content Translation]] now supports dark mode, fulfilling a [[m:Community Wishlist/W544|Community Wishlist request]]. This brings the tool in line with the accessibility features available in the Vector 2022 and Minerva skins, helping reduce visual fatigue for users translating content. [https://phabricator.wikimedia.org/T367077] * DiscussionTools' source mode and the 2017 wikitext editor will now offer autocomplete for links (<bdi lang="zxx" dir="ltr"><code><nowiki>[[</nowiki></code></bdi>), templates (<bdi lang="zxx" dir="ltr"><code><nowiki>{{</nowiki></code></bdi>), HTML and parser tags (<bdi lang="zxx" dir="ltr"><code><nowiki><</nowiki></code></bdi>), and magic words (<bdi lang="zxx" dir="ltr"><code><nowiki>__</nowiki></code></bdi>), making it quicker and easier to insert links, templates, and other wiki markup while editing. [https://phabricator.wikimedia.org/T432400] * The [[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews|Readers Growth team]] has concluded its experiment with mobile page previews and will not roll out the feature. Page Previews are a pop-up bottom sheet that appears when readers tap a blue link, showing a thumbnail, lead paragraph, and an option to open the article. The experiment showed flat retention and negative indicator metrics, suggesting that mobile web readers preferred navigating directly to linked articles rather than using page previews. * The Reader Experience team has seen encouraging early results from the [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Reading Lists feature]], with 93% of participating users reporting that it was useful. Reading Lists help active readers save articles for future reading and support their learning goals on Wikimedia projects. The team plans further improvements before expanding the feature to more users. * The [[mw:Special:MyLanguage/Wikimedia Apps/Team/Explore Feed Refresh|Explore Feed Refresh]] initiative was tested with new and casual Wikipedia app readers. The refreshed feed helps readers discover new and relevant content. After a 10.5% increase in engagement with the feed, Wikimedia Apps team has decided to scale the Home Feed redesign to iOS with the learnings from the Android release applied. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:23}} community-submitted {{PLURAL:23|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where subject names in the Article Guidance feature were displayed with incorrect capitalization on French Wikipedia, has now been fixed. Subject names will now follow the correct capitalization rules for the language. [https://phabricator.wikimedia.org/T427201] '''Updates for technical contributors''' * After running several [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|Account Creation Experiments]] to improve registration completion rates, a new version of the username field on [[Special:CreateAccount|Create Account]] has been rolled out. It includes [[:c:File:Create account - July 2026 updates.png|a popover summarizing the username policy]] to provide clearer guidance during account creation. As part of this change, the messages <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-helpusername</nowiki></code></bdi> and <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-help</nowiki></code></bdi> that several communities have configured will no longer be used. If communities want to customize the guidance shown in the new popover, they can instead edit the following messages: <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet1</nowiki></code></bdi>, <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet2</nowiki></code></bdi>, and <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet3</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T430604] * Later this week, the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|CodeMirror syntax highlighter]] will offer [[w:en:Theme (computing)|themes]]. The themes can be picked from a dropdown menu in the full [[mw:Special:MyLanguage/Help:Extension:CodeMirror#CodeMirror preferences|CodeMirror preferences]] dialog. For wikitext, available themes are default, colorblind-friendly (previously the colorblind preference option on [[Special:Preferences#mw-prefsection-editing]]) and no-highlighting. For code languages (i.e., CSS/JavaScript/JSON/Vue/Lua), there are several themes available. These same themes will eventually be available for wikitext, too. [https://phabricator.wikimedia.org/T163533] * From now on, wikis can restrict editing in the "User" namespace to only the page owner and certain user groups. [[mw:Special:MyLanguage/Manual:$wgRestrictUserPageEditing|Read the configuration documentation]] to learn more. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.14|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/31|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W31"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 juillet 2026 à 20:49 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30856308 --> == Actualités techniques n° 2026-32 == <section begin="technews-2026-W32"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/32|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L’[[mw:Special:MyLanguage/Readers/Reader Experience|équipe Expérience de lecture]] a développé une [https://82db7c8d4b.catalyst.wmcloud.org/w/index.php?title=Regent%27s_Park&uselang=de démonstration d’un correctif] qui entoure la barre d’outils sur deux lignes quand il n’y a pas assez d’espace horizontalement pour tous les boutons. Le but est de réduire l’encombrement de la barre d’outil de Vector 2022 qui se constate parfois sur Wikipédia dans certaines langues avec certaines tailles d’écran. [https://phabricator.wikimedia.org/T429518] * L’équipe Expérience de lecture prévoit de lancer les [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Listes de lecture]], un [[m:Special:MyLanguage/Community Wishlist Survey 2021/Mobile and apps/Have Apps reading lists available on Destop/Mobile|souhait de la communauté]] que vous pouvez déjà essayer en bêta, à partir de septembre. Mais avant cela, l’aide des traducteurs et traductrices est nécessaire pour [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-readinglists&filter=&action=translate plusieurs messages] à traduire dans un maximum de langues. Cette fonctionnalité appuie les objectifs de lecture et d’apprentissage sur Wikipédia. * La semaine prochaine, le sommaire sur les pages des fichiers de Wikimedia Commons sera amélioré en fusionnant le sommaire spécifique aux pages de fichier avec le sommaire classique des pages. Cela rendra plus facile la compréhension de la structure des pages de fichier, la navigation vers une section spécifique et le partage de liens vers une section en particulier. [https://phabricator.wikimedia.org/T332644] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:24|la tâche soumise|les {{formatnum:24}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:24||s}} la semaine dernière]]. Par exemple, certaines images [[w:fr:Tagged Image File Format|TIFF]] ne s’affichaient pas après avoir cliqué sur leur vignette : une image cassé s’affichait au lieu de l’image en taille réelle. Ce problème est désormais corrigé. [https://phabricator.wikimedia.org/T429326] '''Actualités pour la contribution technique''' * Le sélecteur de variables et de fonctions dans les Filtres anti-abus a été mis à jour pour prendre en charge la recherche et l’autocomplétion. Il permettra désormais aux mainteneurs de filtres de retrouver la variable ou fonction désirée plus rapidement. [https://phabricator.wikimedia.org/T323698] * Les formats MJPEG et VP8 sont retirés du lecteur vidéo. Le format MP4 (MPEG-4 Partie 2) est ajouté à la place : il fournit une meilleure qualité pour les vieux modèles d’iPhone. Plusieurs semaines seront peut-être nécessaires pour mettre à jour toutes les vidéos déjà en ligne. Le format par défaut pour les appareils modernes ne change pas (VP9/WebM). [https://phabricator.wikimedia.org/T358266] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.15|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/32|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W32"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 3 août 2026 à 21:46 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30872536 --> == Actualités techniques n° 2026-33 == <section begin="technews-2026-W33"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/33|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] Un nouvel assistant à diagrammes (''ChartWizard'') est [[c:Special:ChartWizard/Data:Example.Pie.chart|désormais disponible sur Wikimedia Commons]] pour les utilisateurs et utilisatrices qui souhaitent créer des graphiques à partir de leurs propres données. Cet assistant rend l’[[mw:Special:MyLanguage/Extension:Chart|extension Chart]] plus accessible aux débutants en permettant la création de diagrammes (par exemple des histogrammes ou des graphiques en secteurs) sans devoir utiliser du JSON. Les utilisateurs peuvent encore repasser à l’éditeur JSON s’ils le préfèrent. Vos retours sur ce nouvel outil sont les bienvenus sur la [[m:Talk:Community Wishlist/W414|page de discussion du souhait]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:19|la tâche soumise|les {{formatnum:19}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:19||s}} la semaine dernière]]. Par exemple, le gadget Photo du jour de l’appli Wikipédia pour iOS affichait par erreur la même image chaque jour au lieu d’en afficher une nouvelle quotidiennement. Cela a désormais été corrigé. [https://phabricator.wikimedia.org/T430692] '''Actualités pour la contribution technique''' * Les images SVG de [[mw:Special:MyLanguage/Extension:Math|formules mathématiques]] seront bientôt générées par le navigateur plutôt que côté serveur. MathML continue d’être généré côté serveur et est rendu dans le navigateur sans JavaScript. Ce changement aura lieu sur Wikibooks le 12 aout, sur Wikisource le 19 aout et sur Wikipédia du 20 au 27 aout. Vous pouvez l’essayer en choisissant « {{int:Mw-math-mathjax}} » dans vos préférences. Cela fait partie de la [[mw:Special:MyLanguage/RESTBase/deprecation|dépréciation de RESTBase]] et [[phab:T431372|de celle de Mathoid]]. [https://phabricator.wikimedia.org/T271001] * <span class="mw-translate-fuzzy">Les pages de catégorie permettront bientôt de trier les pages de la catégorie par ordre chronologique d’ajout dans la catégorie. Cela permettra de retrouver facilement les pages récemment ajoutées ou celles depuis longtemps dans la catégorie. Cela améliorera aussi le traitement des catégories de maintenance, par exemple les demandes de suppression ou d’autres tâches nécessitant un traitement chronologique. Utilisez l’argument <bdi lang="zxx" dir="ltr"><code><nowiki>cldsort=timestamp</nowiki></code></bdi> dans l’URL de la vue d’une catégorie pour trier la liste de pages.</span> [https://phabricator.wikimedia.org/T433768] * Les [[mw:Special:MyLanguage/Extension:Gadgets|gadgets]] et scripts utilisateur sur les wikis Wikimedia peuvent désormais utiliser les [[phab:T395347|fonctionnalités de ES 2018]] et [[phab:T419142|celles de ES 2019]] dans le code JavaScript. Auparavant, la plateforme n’acceptait qu’ES 2017. MediaWiki valide le code source pour protéger les fonctionnalités d’erreurs de syntaxes et pour vérifier que les scripts sont fonctionnels dans tous [[mw:Special:MyLanguage/Compatibility#Browser_support_matrix|les navigateurs pris en charges]]. [https://phabricator.wikimedia.org/T419142] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/33|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W33"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 10 août 2026 à 22:45 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30901051 --> == <span lang="en" dir="ltr">Tech News: 2026-34</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W34"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/34|Translations]] are available. '''Weekly highlight''' * The [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Worklist|Worklist feature]] for the Event Registration tool is now live on all Wikimedia wikis. With Worklist, event organizers can add the articles their event will focus on directly to the event page. The Worklist also powers [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Worklist#How Event Pathways uses the worklist|Event Pathways]] which notifies other editors of the upcoming or ongoing event when they edit an article featured in the event's Worklist. This is the minimum viable version (MVP), and feedback is welcome. Organizers are encouraged to try the feature. A hands-on [[m:Special:MyLanguage/Event:Worklist Setup Workshop: Get Your Event Ready|Worklist Setup Workshop]] will take place on 18 August at 16:00 UTC and 19 August at 11:00 UTC. '''Updates for editors''' * [[Special:ShortPages]] displays short pages by their size, but in many cases it gets filled with disambiguations and soft redirects, making it harder to find the short articles themselves. Starting this weekend, you will be able to choose not to include an article in the special page by adding the magic word <bdi lang="zxx" dir="ltr"><code><nowiki>__EXPECTSHORTPAGE__</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T433203] * One new wiki has been created: a {{int:project-localized-name-group-wikipedia/en}} in [[d:Q3436680|Bole]] ([[w:bol:|<bdi lang="zxx" dir="ltr"><code><nowiki>w:bol:</nowiki></code></bdi>]]) [https://phabricator.wikimedia.org/T429921] * Starting the week of August 17, the page toolbar will wrap onto two lines when there is not enough horizontal space for all the buttons. This is a [[phab:T429518|fully merged patch from the Reader Experience team]] which aims to reduce crowding in the Vector 2022 toolbar, that may occur on some language Wikipedias at certain screen widths. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:16}} community-submitted {{PLURAL:16|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, uploading large files to Wikimedia Commons has become more stable and less prone to failure following some fixes related to the “Could not acquire lock” upload error. [https://phabricator.wikimedia.org/T386640] '''Updates for technical contributors''' * Debian Bullseye will reach the end of its Long Term Support on 31 August 2026. [[phab:T434103|Some Cloud VPS projects]] still have instances running Debian Bullseye. Maintainers of those projects are encouraged to migrate to Debian Bookworm or Debian Trixie. A [[wikitech:Help:Cloud VPS instance operating system migration|migration guide]] is available to help with the process, and users may also want to consider whether their workload is better suited to Toolforge. If you need help or cannot complete the migration by 31 August, please contact the Cloud VPS admins as soon as possible. [[listarchive:list/cloud-announce@lists.wikimedia.org/thread/RVIPQSYLKMSL5M46JP6NEJVGVE6I2RXQ/|Read more]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/34|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W34"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 17 août 2026 à 23:03 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30922806 --> == Actualités techniques n° 2026-35 == <section begin="technews-2026-W35"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/35|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La page [[Special:CreateAccount|Spécial:Créer_un_compte]] a été simplifiée dans le cadre des travaux en cours visant à moderniser le processus de création de compte. Le panneau affichant les statistiques du projet n'apparaît plus à côté du formulaire, que ce soit sur ordinateur ou sur mobile. Plusieurs tests portant sur la création de comptes ont montré qu'un formulaire plus simple aidait les nouveaux utilisateurs à mener à bien leur inscription. [https://phabricator.wikimedia.org/T433783] * Afin d'améliorer les performances de la page, les images ne se chargent désormais que lorsqu'elles sont visualisées. Cela signifie que les images situées plus bas dans un article ne se chargeront pas si le lecteur ne fait jamais défiler la page jusqu'à cette partie, ce qui peut avoir une incidence sur certains indicateurs liés aux images. [https://phabricator.wikimedia.org/T148047] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:42|la tâche soumise|les {{formatnum:42}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:42||s}} la semaine dernière]]. Par exemple, un problème empêchant l'affichage des vignettes d'images sur Abstract Wikipedia après le déplacement du fichier correspondant sur Wikimedia Commons a désormais été résolu. Les vignettes s'actualisent désormais correctement lorsque les fichiers sont déplacés. [https://phabricator.wikimedia.org/T433448] '''Actualités pour la contribution technique''' * La fiche d'informations sur l'utilisateur est une fonctionnalité qui permet aux modérateurs de consulter des informations sur les comptes d'utilisateurs. Jusqu'à présent, elle n'était disponible que dans des sections telles que l'historique des pages, les journaux et les modifications récentes. Désormais, il est également possible de [[mw:Special:MyLanguage/Help:Extension:CheckUser#User_Info_card_in_page_content|l'insérer dans le contenu d'une page]] à l'aide de la fonction d'analyse <bdi lang="zxx" dir="ltr"><code><nowiki>{{#uic:}}</nowiki></code></bdi>. Cela peut s'avérer particulièrement utile dans des modèles tels que [[:en:Template:Userlinks|<bdi lang="zxx" dir="ltr"><code><nowiki>{{Userlinks}}</nowiki></code></bdi>]] (ou leurs variantes spécialisées), car cela facilitera la compréhension du contexte relatif à un ou une utilisatrice sur les divers forums communautaires. La fiche ne s'affichera que pour les utilisateurs qui l'ont activée dans leurs [[Special:Preferences#mw-input-wpcheckuser-userinfocard-enable|préférences]]. [https://phabricator.wikimedia.org/T424466] * En raison des risques liés à la sécurité et à la confidentialité des utilisateurs, nous avons désactivé l'accès aux URL utilisant <bdi lang="zxx" dir="ltr"><code><nowiki>Special:MyPage</nowiki></code></bdi> (Spécial:Ma_page) avec l’argument <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. Si vous êtes concerné par cette mesure, demandez-vous s'il est possible d'adopter une autre approche. Les URL utilisant <bdi lang="zxx" dir="ltr"><code><nowiki>Special:MyPage</nowiki></code></bdi> restent accessibles et utilisables sans <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. Les URL de pages utilisateur spécifiées (par exemple, <bdi lang="zxx" dir="ltr"><code><nowiki>User:Myusername</nowiki></code></bdi>) peuvent toujours être utilisées avec <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T120386] * Suite à une mise à jour, le logiciel de création de vignettes a été amélioré. En effet, <bdi lang="zxx" dir="ltr"><code><nowiki>librsvg</nowiki></code></bdi> a été mis à jour vers la version 2.60 et <bdi lang="zxx" dir="ltr"><code><nowiki>ImageMagick</nowiki></code></bdi> vers la version 7. Cela a permis la résolution d’un certain nombre de bugs de longue date liés à la création de vignettes, tels que des erreurs de rendu. [https://phabricator.wikimedia.org/T419815#12222841] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.17|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/35|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W35"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 24 août 2026 à 22:45 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30961208 --> == Actualités techniques n° 2026-36 == <section begin="technews-2026-W36"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/36|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Un nouveau format pour la liste de souhaits de la communauté est ouvert aux commentaires. Vous pouvez [[m:Special:MyLanguage/Community Wishlist/Community Wishlist 2027|lire les idées proposées sur Meta]]. Ce nouveau processus prévoit d'améliorer la manière dont les souhaits sont triés, votés et priorisés d'une manière transparente et équilibrée entre les familles de projets et les éditions linguistiques. Cette consultation est ouverte pendant deux semaines. '''Actualités pour la contribution''' * La dernière version de l'application Wikipédia pour Android comprend des mises à jour de la fonctionnalité «Enregistrés», qui rapprochent l'expérience d'enregistrement de l'application de celle proposée sur iOS et sur le Web. Cette mise à jour repense l'onglet «Enregistrés» en proposant une vue «Tous les articles», supprime la liste de lecture par défaut «Enregistrés», renomme les listes de lecture «Collections» et modernise l'expérience de sauvegarde des articles. [https://phabricator.wikimedia.org/T420788] * La fonctionnalité [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Listes de lecture]] a été activée pour tous les utilisateurs connectés sur les versions de Wikipédia en bengali, chinois, tchèque et vietnamien le 25 août, après plusieurs mois en tant que fonctionnalité bêta. Les listes de lecture seront disponibles pour tous les utilisateurs connectés sur les versions de Wikipédia en arabe, français et indonésien le 1er septembre, suivies par la Wikipédia en anglais le 14 septembre, et toutes les autres versions de Wikipédia le 28 septembre. * À la fin du mois, certains lecteurs non connectés sur les versions en bengali, tchèque, persan, anglais et polonais de Wikipédia utilisant l'habillage Minerva sur mobile verront une [[mw:Special:MyLanguage/Readers/Reader_Growth/Minimal_Minerva|barre de navigation mise à jour]] dans le cadre d'un [[w:A/B test|test A/B]]. Le test comparera la barre de navigation actuelle avec une nouvelle version conçue pour faciliter la recherche d'informations plus rapidement. L'objectif est de déterminer si ces changements encouragent les lecteurs à revenir plus souvent. Cette expérience n'aura aucune incidence sur l'expérience des lecteurs et/ou des rédacteurs connectés. * Les contributeurs qui gèrent les redirections, les modèles et les catégories utilisés sur les pages de redirection disposent désormais de meilleurs moyens pour trouver et organiser les redirections. Auparavant, il n'était pas possible de rechercher les pages de redirection. Deux nouveaux mots-clés de recherche, <bdi lang="zxx" dir="ltr"><code><nowiki>onlyredirects:</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>withredirects:</nowiki></code></bdi>, permettent désormais de rechercher directement les redirections et peuvent être combinés avec des mots-clés existants tels que <bdi lang="zxx" dir="ltr"><code><nowiki>incategory:</nowiki></code></bdi>, <bdi lang="zxx" dir="ltr"><code><nowiki>intitle:</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>insource:</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T204089] * Les outils de recherche ISBN pour générer des citations ne fonctionnaient pas récemment en raison de problèmes de service externe. Les développeurs travaillent sur des solutions. [https://phabricator.wikimedia.org/T435179] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, un problème où la recherche de pages par catégorie à l'aide de <bdi lang="zxx" dir="ltr"><code><nowiki>deepcat</nowiki></code></bdi> pouvait ne renvoyer aucun résultat ou des résultats sans rapport a été corrigé. [https://phabricator.wikimedia.org/T414859] '''Actualités pour la contribution technique''' * Le domaine des URL pour les miniatures passe de upload.wikimedia.org à thumb.wikimedia.org. Les anciennes URL continueront de fonctionner pour le moment, mais MediaWiki utilisera le nouveau domaine à la place. Les URL vers d'autres types de médias tels que les fichiers originaux, les vidéos et les transcodages seront toujours servies depuis upload.wikimedia.org. [https://phabricator.wikimedia.org/T427465] * L'API Math de Wikimédia [https://www.mediawiki.org/w/index.php?api=wmf-math%2Fv1&title=Special%3ARestSandbox] est désormais obsolète. Ces points de terminaison seront complètement arrêtés d’ici la fin de septembre 2026. Les développeurs qui appellent ces points de terminaison devraient passer à des solutions alternatives de rendu mathématique, telles que le [https://developer.mozilla.org/en-US/docs/Web/MathML MathML] natif ou [https://www.mathjax.org/ MathJax]. Les installations tierces de MediaWiki utilisant l'extension Math pour le rendu des formules doivent effectuer une mise à jour vers la version 1.43+ afin d'éviter toute interruption de service. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.18|MediaWiki]] '''En détails''' * Lisez-en plus sur [[mw:Special:MyLanguage/Edit_check/TextMatch|TextMatch]] dans un article de Diff intitulé [[diffblog:2026/08/28/custom-edit-suggestions-for-every-wiki-how-communities-are-shaping-suggestion-mode-with-textmatch/|Suggestions de modifications personnalisées pour chaque wiki : comment les communautés façonnent le mode Suggestion avec TextMatch]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/36|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W36"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 31 août 2026 à 22:53 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30990659 --> == Actualités techniques n° 2026-37 == <section begin="technews-2026-W37"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/37|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La fonctionnalité [[mw:Special:MyLanguage/Help:Growth/Tools/Add_a_link|Ajouter un lien]] a été améliorée pour les utilisateurs de Wikipédia en anglais, avec une meilleure détection des liens présentant des différences de majuscules. Cela permettra de réduire les suggestions ambiguës dues aux différences de majuscules dans les titres d'articles, notamment ceux consacrés aux biens culturels. Une deuxième phase d'améliorations est prévue, qui permettra également de préparer cette fonctionnalité en vue de son déploiement dans d'autres langues. [https://phabricator.wikimedia.org/T435526#12240876] [https://phabricator.wikimedia.org/T434259] * La fonctionnalité [[mw:Special:MyLanguage/Article_guidance|Guide des articles]] sera activée par défaut pour les contributeurs débutants sur Wikipédia en anglais simplifié et en turc à compter du 10 septembre 2026, à la suite d'[[mw:Special:MyLanguage/Article_guidance/Updates#Summary_of_experiment_result|une expérience]]. Les contributeurs débutants ayant effectué entre 0 et 99 modifications verront automatiquement s'afficher cette fonctionnalité lorsqu'ils cliqueront sur un lien rouge ou utiliseront l'option « [[tr:Vikipedi:Madde_sihirbazı|Madde oluşturto]] » sur Wikipédia en turc pour créer un nouvel article. Ce changement vise à aider les contributeurs débutants à créer des articles de meilleure qualité, conformes aux normes de chaque Wikipédia. [[phab:maniphest/query/z3tcTjmLMxk5/#R|D'autres améliorations]] seront apportées en fonction des résultats de l'expérience et des retours de la communauté. * L'outil [https://pageviews.wmcloud.org « Analyse des pages vues »], qui permet aux utilisateurs de comparer le nombre de pages vues sur plusieurs pages, fête cette année ses 10 ans et s'est enrichi de plusieurs nouvelles fonctionnalités. Parmi celles-ci figurent [[toolforge:wikinav|WikiNav]], qui donne un aperçu de la manière dont les lecteurs de Wikipédia explorent le contenu, les statistiques d'édition dans [https://pageviews.wmcloud.org/siteviews?range=latest-30&sites=en.wikipedia.org « Vues du site »], la possibilité de [https://pageviews.wmcloud.org/massviews?source=wikiproject&project=en.wikipedia.org rechercher des articles appartenant à un projet Wiki] et la prise en charge du mode sombre. [https://phabricator.wikimedia.org/T378549] * Une barre de navigation [[mw:Special:MyLanguage/Readers/Reader_Growth/Minimal_Minerva|Minerva, visuellement simplifiée]] est actuellement testée auprès des lecteurs non connectés utilisant la version mobile des Wikipédias en bengali, tchèque, anglais, farsi et polonais. Cette expérience vise à déterminer si la simplification de la navigation permet de fidéliser davantage les lecteurs. Le test se déroulera du 31 août au 28 septembre et ne nécessite aucune intervention de la part des utilisateurs. * [[m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]] travaille à l'amélioration des [[en:Wikipedia:VisualEditor/Named references|noms de référence générés automatiquement dans VisualEditor]]. Les contributeurs ne remarqueront qu'un léger changement à partir de cette semaine. Lors de l'ajout de noms de référence automatiques, la numérotation commencera par <bdi lang="zxx" dir="ltr"><code><nowiki>:1</nowiki></code></bdi> au lieu de <bdi lang="zxx" dir="ltr"><code><nowiki>:0</nowiki></code></bdi>. Pour en savoir plus, consultez la [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names|page du projet]]. [https://gerrit.wikimedia.org/r/c/VisualEditor/VisualEditor/+/1332704] * Tous les wikis seront [[m:Special:MyLanguage/Tech/Server switch|en lecture seule pendant quelques minutes]] le 23 septembre. Cette opération est prévue à 14h UTC. De plus amples informations seront publiées dans la rubrique « Tech News » et seront également mises en ligne sur chaque wiki au cours des prochaines semaines. [https://phabricator.wikimedia.org/T433363] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:25|la tâche soumise|les {{formatnum:25}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:25||s}} la semaine dernière]]. Par exemple, un problème lié au fait que [[mw:Special:MyLanguage/Parsoid|Parsoid]] pouvait mal gérer les balises nowiki imbriquées, entraînant la perte de contenu et l'affichage de texte indésirable, a désormais été corrigé. [https://phabricator.wikimedia.org/T435116] '''Actualités pour la contribution technique''' * Les administrateurs de l'interface peuvent configurer les gadgets à partir de [[MediaWiki:Gadgets-definition]]. Le format de définition a été mis à jour et ne nécessite plus l'option <bdi lang="zxx" dir="ltr"><code><nowiki>ResourceLoader</nowiki></code></bdi>, car les gadgets sont toujours chargés via <bdi lang="zxx" dir="ltr"><code><nowiki>ResourceLoader</nowiki></code></bdi>. Cela simplifie la configuration des gadgets en supprimant une option qui n'est plus nécessaire, ce qui facilite la tâche des administrateurs lors de la définition des gadgets. [https://phabricator.wikimedia.org/T298199] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.19|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/37|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W37"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 7 septembre 2026 à 20:44 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31008437 --> == <span lang="en" dir="ltr">Tech News: 2026-38</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W38"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/38|Translations]] are available. '''Weekly highlight''' * The Future Audiences team has started a [[en:Special:MyLanguage/Wikipedia:Village pump (proposals)#New proposed experiment to let readers know about “Google preferred sources”|discussion on English Wikipedia]] about a proposed new experiment to show a temporary notice about the new [https://blog.google/products-and-platforms/products/search/original-high-quality-content-search/ Google Preferred Sources] feature to Wikipedia readers coming from Google. The experiment is to test whether this notice makes visitors come back to Wikipedia more often. Preferred Sources feature lets users choose websites they trust so Google can highlight content from those sources more prominently in Search and AI experiences. We are interested in testing this on other languages that Google supports and would welcome assistance with starting conversations on other wikis. If interested, please reach out to Future Audiences on the [[m:Special:MyLanguage/Future Audiences/Preferred sources|talk page]]. '''Updates for editors''' * The chat platform Discord is releasing a new self-service framework that websites can use to specify how their links should look when shared on Discord. The Future Audiences team is planning to start using this new framework to improve how Wikipedia links look when shared on Discord, giving better attribution and credit to contributors. The link appearance and behavior won't change in the first phase of this project as we want to first collect some baseline data to assess the impact of future changes. However, if community members who are active on Discord spot any issues, they can reach out on [[phab:tag/future-audiences/|Phabricator]] or [[m:Special:MyLanguage/Future Audiences|Metawiki]]. * The Reader Growth team will be running an experiment to test whether [[mw:Special:MyLanguage/Readers/Reader_Growth/Compact Lead|compacting article lead sections on mobiles]] with the addition of a "read more" button improves reader retention. The test will begin on September 17 on Arabic, Spanish, French, Indonesian, Italian, Japanese, Portuguese, Vietnamese, and Chinese Wikipedias and will run for four weeks. * All wikis will be [[m:Special:MyLanguage/Tech/Server switch|read-only for a few minutes]] on September 23. This is planned at 14:00 UTC. More information will be published in Tech News and will also be posted on individual wikis in the coming week. [https://phabricator.wikimedia.org/T433363] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where [https://wikistats.wmcloud.org/ Wikistats] for Wikipedias was returning an HTTP 500 error and could not be reached, has now been fixed. [https://phabricator.wikimedia.org/T435959] '''Updates for technical contributors''' * Developers who maintain a tool that queries the Wikimedia Commons links tables need to update their code to connect to the new x4 database cluster. The links tables have been moved from the s4 cluster to x4, and will no longer receive updates on s4. The page and redirect tables remain available on both clusters. A wiki replica for the x4 cluster will be set up afterwards. The change is being made because the s4 cluster has grown too large to operate efficiently. You can [[wikitech:Special:MyLanguage/News/2026 Commons links tables database split|read more]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.20|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/38|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W38"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 14 septembre 2026 à 17:31 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31039826 --> == Bascule de serveur : votre wiki sera bientôt en lecture seule durant un court instant == <section begin="server-switch"/><div class="plainlinks"> [[:m:Special:MyLanguage/Tech/Server switch|Lire ce message dans une autre langue]] • [https://meta.wikimedia.org/w/index.php?title=Special:Translate&group=page-Tech%2FServer+switch&language=&action=page&filter= {{int:please-translate}}] La [[foundation:|Fondation Wikimédia]] va basculer le trafic entre ses centres de données. Cela permettra de s’assurer que Wikipédia et les autres wikis de Wikimédia peuvent rester en ligne même après une catastrophe. Le trafic sera basculé le '''{{#time:j xg|2026-09-23|fr}}'''. La bascule débutera à '''[https://zonestamp.toolforge.org/{{#time:U|2026-09-23T14:00|en}} {{#time:H:i e|2026-09-23T14:00}}]'''. Malheureusement, en raison de certaines limites de [[mw:Special:MyLanguage/Manual:What is MediaWiki?|MediaWiki]], toutes les modifications de pages devront être arrêtées durant le passage d’un centre de données à l’autre. Nous nous excusons pour ce dérangement, que nous nous efforçons de réduire pour le futur. Une bannière sera affichée sur tous les wikis 30 minutes avant le début de l’opération. Cette bannière restera visible jusqu’à la fin de l’opération. Vous pouvez contribuer à la [https://meta.wikimedia.org/w/index.php?title=Special%3ATranslate&group=Centralnotice-tgroup-read_only_banner&task=view&language=&filter=&action=translate traduction ou relecture] du texte de cette bannière. '''Pendant une courte période, vous pourrez lire les wikis mais pas les modifier.''' * Vous ne pourrez pas effectuer de modification pendant une durée pouvant aller jusqu’à une heure, le {{#time:l j xg Y|2026-09-23|fr}}. * Si vous essayez de faire une modification ou de sauvegarder pendant cette période, vous verrez un message d’erreur. Nous espérons qu’aucune modification ne sera perdue durant ce temps, mais nous ne pouvons le garantir. Si vous voyez un message d’erreur, merci de patienter jusqu’au retour à la normale. Vous pourrez alors enregistrer votre modification. Nous vous conseillons cependant de faire une copie de votre modification avant, au cas où. ''Autres conséquences :'' * Les tâches de fond seront ralenties et certaines pourraient être stoppées. Les liens rouges ne seront pas mis à jour aussi vite que d’habitude. Si vous créez un article qui est déjà lié depuis une autre page, le lien rouge pourrait rester rouge plus longtemps que d’habitude. Certains scripts ayant un long temps d’exécution devront être stoppés. * Le déploiement de code devrait se dérouler comme chaque semaine. Cependant, certains codes particuliers pourraient être gelés si l’opération le nécessitait. * [[mw:Special:MyLanguage/GitLab|GitLab]] sera indisponible durant environ 90 minutes. Ce projet pourra être reporté si nécessaire. Vous pouvez [[wikitech:Switch_Datacenter|consulter le calendrier sur wikitech.wikimedia.org]]. Tout changement sera annoncé dans le calendrier. '''Merci de partager ces informations avec votre communauté.'''</div><section end="server-switch"/> [[m:user:Trizek (WMF)|Trizek (WMF)]] 15 septembre 2026 à 14:58 (CEST) <!-- Message envoyé par User:Trizek (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=30513874 --> == Your feedback wanted: Remove ''(Language link added:) and similar'' edits from Special:RecentChanges == ''(Apologies for posting in English. You can help by translating it)'' Hello, I’m Danny from [[:en:w:Wikimedia_Deutschland|Wikimedia Deutschland’s]] [[m:Wikidata_For_Wikimedia_Projects|Wikidata For Wikimedia Projects]] team.<br/> We have been investigating and examining how Wikidata’s use in the [[Special:RecentChanges]] and [[Special:Watchlist]] pages can be made to be more useful, or less ‘noisy’ (fewer edits that have little value to the reader). We are currently exploring to make a change in the volume and the type of Wikidata edits that appear in these pages, and '''we need your feedback''' to help us ensure this is a positive change for the Wikis and won’t disrupt your workflows.<br/> Language links are also known as sitelinks or interlanguage links and describe when a Wikidata item is connected to a Wiki, adding it to the language dropdown tool for other connected Wikis. [[File:Types of Lang Link changes.png|x350px|frame|A variety of "Language Link" edits as seen in the Recent Changes feed]] The request for feedback is whether you support the removal of these language link edits from the Recent Changes and Watchlist feed, or would you oppose such a change and if so, why? Please share you thoughts on the dedicated '''[[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|metawiki Talk page]]'''. Further information about this change: [[m:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|m:Wikidata_For_Wikimedia_Projects/Hide_Language_Link_edits]] Thank you for any participation or feedback you give, -- [[m:User:Danny_Benjafield_(WMDE)|User:Danny Benjafield (WMDE)]] 25 septembre 2026 à 13:46 (CEST) <!-- Message envoyé par User:Danny Benjafield (WMDE)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Danny_Benjafield_(WMDE)/MassMessage_test_list&oldid=31080832 --> == Actualités techniques n° 2026-39 == <section begin="technews-2026-W39"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/39|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * [[m:Special:MyLanguage/Tech/Server switch|Tous les wikis seront en lecture seule]] pendant quelques minutes le mercredi 23 septembre 2026 à [https://zonestamp.toolforge.org/1790172000 14 h 00 UTC]. Cette interruption est nécessaire pour effectuer des tests de sauvegarde dans le cadre de la bascule des serveurs du centre de données, [[wikitech:Special:MyLanguage/Deployments/Yearly calendar|qui a lieu deux fois par an]]. Pendant cette bascule, tout le trafic des sites web de Wikimédia est redirigé d'un centre de données principal vers le centre de données de secours afin de tester la disponibilité et d'éviter toute interruption de service, même en cas d'urgence. [https://phabricator.wikimedia.org/T433363] '''Actualités pour la contribution''' * L'équipe Growth a testé un nouvel avis post-édition destiné à [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#4. Encourage Temporary Accounts to Register|encourager les titulaires de comptes temporaires à créer un compte permanent]]. Ce nouvel avis a remplacé plusieurs boîtes de dialogue et une notification de bienvenue simultanée par un message unique mettant en avant les avantages de la création d'un compte. L'expérience a permis d'augmenter le taux de création de comptes permanents de 1,94 % à 3,66 %, soit une hausse relative de 87 %. Cette modification va désormais être déployée sur tous les wikis. [https://phabricator.wikimedia.org/T433551] * Pour les contributeurs de Wikipédia utilisant la [[Special:Preferences#mw-prefsection-betafeatures|fonctionnalité bêta « Mode suggestion »]], il existe une [[Special:Preferences#mw-input-wpvisualeditor-editcheck-experimental|nouvelle préférence utilisateur à activer]] permettant d’afficher des contrôles d’édition et des suggestions « expérimentaux ». Ces types sont répertoriés sur la page [[Special:EditChecks#experimental-checks|Special:EditChecks]] et sont destinés aux premiers tests par les développeurs ainsi qu’à la collecte de retours d’expérience de la part d’utilisateurs expérimentés. Les administrateurs peuvent modifier les [[mw:Special:MyLanguage/Edit check/Configuration|détails de configuration]] de chaque suggestion comme d'habitude. Ils peuvent également [[mw:Special:MyLanguage/Help:Suggestion mode#Experimental Suggestions|utiliser cette fonctionnalité]] pour tester les idées de leur communauté concernant des contrôles et des suggestions créés localement. Les types expérimentaux bénéficieront d'un style visuel plus distinct dès la fin de cette semaine. [[mw:Talk:VisualEditor/Suggestion Mode|Vos retours sont les bienvenus]]. * Le [[m:CEE Technical Village Pump|CEE Technical Village Pump]] a été lancé afin d'offrir un espace dédié aux discussions techniques et à la collaboration entre les communautés Wikimédia d'Europe centrale et orientale. * Un nouvel outil, [[mw:Special:MyLanguage/Language Onboarding and Development/Starter kit|Starter Kit]], est désormais disponible pour les communautés Wikipédia de langues nouvelles ou peu répandues. Il rassemble des tâches guidées, des outils et des ressources destinés à aider les communautés à se lancer, à suivre leurs progrès et à collaborer avec l'ensemble de la communauté Wikimédia. Le Starter Kit est hébergé sur Wikimedia Toolforge et est destiné aux Wikipédias des langues comptant moins de 50 000 articles. Pour en savoir plus sur le fonctionnement du Starter Kit et sur la manière dont les communautés l'utilisent, consultez [[diffblog:2026/09/18/introducing-starter-kit-for-new-and-small-language-wikipedias/|cet article du blog Diff]]. * L'équipe chargée de la croissance du nombre de lecteurs lance une nouvelle phase de test de l'[[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|expérience sur le carrousel d'images]]. Ce nouveau test utilisera trois nouvelles versions du design, mises à jour en fonction des commentaires de la communauté. L'équipe évaluera les résultats et déterminera avec les communautés s'il convient ou non de mettre en place cette fonctionnalité. L'expérience débutera la semaine du 28 septembre et se déroulera sur les Wikipédias en arabe, bengali, chinois, tchèque, anglais, farsi, français, allemand, indonésien, japonais, polonais, portugais, espagnol, suédois et vietnamien. * Après avoir été déployée avec succès sur les Wikipédias en arabe, en bengali, en chinois, en tchèque, en anglais, en français, en indonésien et en vietnamien, la [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|fonctionnalité « Listes de lecture »]] développée par l'équipe chargée de l'expérience utilisateur sera accessible à tous les utilisateurs connectés sur l'ensemble des wikis Wikipédia à compter du 28 septembre 2026. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:28|la tâche soumise|les {{formatnum:28}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:28||s}} la semaine dernière]]. Par exemple, un problème lié au script de statut des mentors, qui mettait à jour de manière répétée le statut des mentors alors qu'aucun changement n'était intervenu, générant ainsi des entrées inutiles dans la section « Modifications récentes », a désormais été corrigé. [https://phabricator.wikimedia.org/T436659] '''Actualités pour la contribution technique''' * L'API de requête de liste <bdi lang="zxx" dir="ltr"><code><nowiki>abusefilters</nowiki></code></bdi>, utilisée pour récupérer des informations spécifiques sur les filtres anti-abus actifs ou historiques configurés sur un wiki, a été mise à jour afin de prendre en charge le paramètre de configuration <bdi lang="zxx" dir="ltr"><code><nowiki>formatversion=2</nowiki></code></bdi>, ce qui entraîne des changements majeurs dans la réponse de l'API. Tous les utilisateurs qui gèrent des scripts utilisateur ou tout autre code doivent vérifier s'ils utilisent l'API de requête de liste <bdi lang="zxx" dir="ltr"><code><nowiki>abusefilters</nowiki></code></bdi> et s'adapter à ces changements majeurs. [https://phabricator.wikimedia.org/T435828] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.21|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/39|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W39"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 septembre 2026 à 13:56 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31085309 --> == Actualités techniques n° 2026-40 == <section begin="technews-2026-W40"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/40|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Les utilisateurs des skins par défaut sur le web pour ordinateur et mobile (respectivement Vector 2022 et Minerva) seront désormais informés si les suggestions de recherche affichées pendant la saisie dans la Recherche proviennent de redirections de pages. Auparavant, les utilisateurs pouvaient être confus lorsque les principales suggestions de recherche ne correspondaient pas exactement à ce qu'ils avaient tapé. La nouvelle notification de redirection précise que ces suggestions ne sont pas aléatoires, mais liées à la recherche. Les pages suggérées peuvent être des redirections vers les articles les plus pertinents recherchés. [https://phabricator.wikimedia.org/T303013] * Les utilisateurs de Wikipédia déconnectés sur le web mobile verront bientôt un bouton de signet pour enregistrer des articles dans leurs [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|listes de lecture]] à la place du bouton Watchstar actuel. Lorsqu'on clique dessus, le bouton de signet invite les utilisateurs à se connecter ou à créer un compte pour enregistrer un article pour plus tard. Une expérience précédente a montré que l'icône de signet incitait quatre fois plus les utilisateurs déconnectés à créer un compte par rapport au bouton Watchstar. Ce changement fait partie des efforts pour encourager plus de lecteurs à devenir titulaires d'un compte et à utiliser des fonctionnalités qui les aident à sauvegarder et à revenir au contenu. [https://phabricator.wikimedia.org/T438769] * Une expérience commencera le 29 septembre sur les Wikipédias en espagnol, arabe, anglais et français pour simplifier l'expérience que les utilisateurs rencontrent immédiatement après la création de leur compte. Le test permettra de voir si réduire les obstacles du sondage de bienvenue actuel aide les nouveaux utilisateurs à comprendre plus facilement ce qu'ils doivent faire ensuite. L'expérience se déroulera jusqu'à la fin du mois d'octobre. [https://phabricator.wikimedia.org/T430058] * Le [[m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif sur les produits et la technologie]] a lancé un appel à la communauté pour suggérer des sujets qu'il devrait examiner et discuter avec la Wikimedia Foundation. Les éditeurs et contributeurs techniques sont invités à ajouter des sujets à [[m:Talk:Product and Technology Advisory Council|la page de discussion du conseil]]. * A la fin de cette semaine, il sera possible pour les communautés de configurer des [[mw:Special:MyLanguage/Help:Edit check|Vérifications et suggestions]] afin qu'elles soient [[mw:Special:MyLanguage/Edit check/Configuration#extraNamespaces|affichées dans des espaces de noms supplémentaires]], comme un espace de noms <bdi lang="zxx" dir="ltr"><code><nowiki>Draft:</nowiki></code></bdi>. [[mw:Talk:VisualEditor/Suggestion Mode|Vos retours sont les bienvenus]]. * Un bug dans la [[m:Special:GlobalWatchlist|Liste globale de surveillance]] empêche la page de se charger pour les utilisateurs qui ont des modifications non vues sur Wikimedia Commons. Les développeurs travaillent sur une solution. En attendant, les utilisateurs concernés peuvent suivre les solutions de contournement décrites dans la section d'aide [[mw:Special:MyLanguage/Extension:GlobalWatchlist#GlobalWatchlist temporarily broken for users with Wikimedia Commons|pertinente]]. * Les nouveaux mots magiques <bdi lang="zxx" dir="ltr"><code><nowiki>{{CATEGORYSORT:TIMESTAMP}}</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>{{CATEGORYSORT:RTIMESTAMP}}</nowiki></code></bdi> sont maintenant disponibles. Ils permettent de changer le tri par défaut des catégories pour qu'il soit basé sur la date et l'heure de la catégorisation. [https://phabricator.wikimedia.org/T433768] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:20|la tâche soumise|les {{formatnum:20}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:20||s}} la semaine dernière]]. Par exemple, le problème où [https://wikistats.wmcloud.org/ Wikistats] pour Wikipédia n'était pas accessible a maintenant été résolu. [https://phabricator.wikimedia.org/T435959] '''Actualités pour la contribution technique''' * Le basculement du centre des données, prévu le 23 septembre, a été reporté. [[diffblog:2025/03/12/hear-that-the-wikis-go-silent-twice-a-year/|L'exercice de l'équinoxe]] a révélé des problèmes de capacité, empêchant le transfert de tous les services vers l'autre centre des données. Le processus a été mis en pause pour se concentrer sur l'investigation de ce problème, afin de continuer à pouvoir servir nos utilisateurs de manière fiable. Un nouvel exercice sera planifié. En attendant, tout le trafic et les modifications continuent de fonctionner normalement. * Le 23 septembre, une panne de courant a brièvement affecté tous les wikis, créant des problèmes intermittents en mode lecture et en mode édition. Cela a aussi affecté Gerrit. Cela n'a aucun lien avec le report du basculement du centre de données. [https://www.wikimediastatus.net/incidents/9fm19t8qykk8] * [[mw:Special:MyLanguage/Help:Extension:Produnto|Produnto]] a été déployé sur mediawiki.org et [[phab:T421436|certaines wikis en langues indiquées]]. Nous voulons avoir vos retours sur ce produit expérimental. Produnto permet aux utilisateurs d'utiliser des modules Lua hébergés sur [[gitlab:repos/lua|GitLab de Wikimedia]]. Il simplifie le partage du code Lua entre les wikis. Au lieu de copier chaque module Lua individuellement d'un wiki à l'autre, les utilisateurs pourront facilement partager des packages entre tous les wikis. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.22|MediaWiki]] '''Rencontres et évènements''' * Un [[m:Event:Cross-wiki Code Collaboration Workshop|atelier de collaboration sur le code entre wikis]] aura lieu en ligne le 2 octobre à 12h30 UTC, réunissant des contributeurs techniques d'Asie du Sud pour découvrir Produnto et commencer à le tester sur les Wikipédias en Hindi, Punjabi, Odia, Telugu et Malayalam. Produnto est un gestionnaire de paquets pour déployer des modules Lua hébergés sur GitLab vers les wikis Wikimedia, et a récemment été déployé sur certains wikis pilotes. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/40|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W40"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 28 septembre 2026 à 12:49 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31092225 --> == Wikidata Sitelink changes in Recent Changes and Watchlist == ''(Apologies for cross-posting in English, translating it will help spread the message)'' Editors on group 1 wikis, including Wikiquote, Wikibooks, Wikiversity, Wikivoyage, and Wikisource, will no longer see notifications on [[Special:RecentChanges|Recent Changes]] and [[Special:Watchlist|Watchlist]] when a link to ''another'' language version of a page is added, removed, or changed in its Wikidata item. This change will begin on '''October 5 at 07:00 UTC''' as a two-week trial to reduce high-volume, low-value notifications and make higher-value changes easier to identify. Editors who rely on these notifications should be aware of the change and can share feedback or concerns on the [[m:Wikidata For Wikimedia Projects/Clearer Wikidata Edit Summaries/Hide Language Link edits|project]] or [[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|talk page]].<br/>Please reach out if you have any questions, thank you! -- [[User:Danny_Benjafield_(WMDE)|Danny Benjafield (WMDE)]] ([[User_talk:Danny_Benjafield_(WMDE)|talk]]) sent via [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 1 octobre 2026 à 14:42 (CEST) <!-- Message envoyé par User:Danny Benjafield (WMDE)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Danny_Benjafield_(WMDE)/MassMessage_test_list&oldid=31106967 --> == Actualités techniques n° 2026-41 == <section begin="technews-2026-W41"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/41|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le processus relatif à la liste de souhaits communautaires de cette année [[m:Special:MyLanguage/Community Wishlist/Community Wishlist 2027#Updated Community Wishlist Process 2027/2028|a été publié]], en tenant compte des commentaires de la communauté. La période de soumission des souhaits [[m:Special:MyLanguage/Community Wishlist/FAQ#What is the deadline for submitting wishes?|prendra fin le 8 novembre]]. Cette année, [[m:Special:MyLanguage/Community Wishlist/Community Wishlist 2027/Working group|un groupe de travail (appel à volontariat ouvert)]] composé de membres de la communauté connaissant bien les aspects techniques de nos wikis et de membres du personnel de la Fondation Wikimedia se chargera du [[m:Special:MyLanguage/Community Wishlist/FAQ#What is triage?|processus de triage]]. Le triage devrait être terminé d'ici la fin novembre. Le vote sur les souhaits débutera en janvier. '''Actualités pour la contribution''' * Les utilisateurs adeptes de l'[[mw:Special:MyLanguage/VisualEditor|éditeur visuel]] verront maintenant un avertissement lorsqu'ils collent du texte qui a probablement été copié d'une de 5 IA génératives dans Wikipédia. Cette fonctionalité est connue sous le nom de [[mw:Special:MyLanguage/Edit check/LLM Paste Check|Vérification du collage de LLM]]. C'est un type de [[mw:Special:MyLanguage/Edit check|Vérification des modifications]] qui est configurable par communauté. La vérification cherche des marques dans l'HTML copié et n'utilise pas de la détection de tatouage numérique (watermark). Les modifications seront aussi [[mw:Special:MyLanguage/Edit check/LLM Paste Check#Edit tags|marquées]] pour que les patrouilleurs puissent les revoir en détail. Cette fonctionalité aide les nouveaux ajouter du contenu qui respecte les règles de Wikipédia et rend l'identification de contributions problèmatiques plus facile pour les utilisateurs expérimentés. Les communautés avec des règles locales sur l'usage de LLM peuvent [[mw:Special:MyLanguage/Edit check/LLM Paste Check#Configuration|ajuster la formulation et les règles liées]]. L'équipe s'[[listarchive:list/translators-l@lists.wikimedia.org/message/EVHKWJ4IERZELMPIWNH3XUVUSQV5KOO4/|excuse]] que cette fonctionalité a été poussée sans un délai pour les traductions. * Les utilisateurs peuvent désormais [[mw:Special:MyLanguage/Wikimedia Apps/Team/Future of Editing on the Mobile Apps|accéder facilement à l'éditeur visuel web directement depuis les applications iOS et Android]]. À l'ouverture de l'éditeur, les utilisateurs ont le choix entre l'édition visuelle et l'édition source. S'ils optent pour l'édition visuelle, ils sont redirigés vers une page web mobile dans leur navigateur internet. Une fois la modification terminée, une bannière leur permet de revenir à l'application. Cela permet aux lecteurs d'apporter rapidement des contributions tout en consultant le contenu, et facilite la transition vers la version web mobile. * Les utilisateurs des applications Android et iOS de Wikipédia verront désormais une proposition unique de création d'un compte après avoir utilisé l'application pendant deux semaines consécutives. Cette bannière explique les avantages d'avoir un compte. Il est destiné à aider les lecteurs occasionnels à devenir des utilisateurs connectés qui reviennent régulièrement aux applications. [https://phabricator.wikimedia.org/T434847] * Les contributeurs à des [[wmnoc:conf/dblists/group1.dblist|wikis de groupe 1]] (dont Wikiquote, Wikibooks, Wikiversity, Wikivoyage et Wikisource) ne verront plus de notifications dans les [[mw:Special:MyLanguage/Help:Recent changes|modifications récentes]] et la [[mw:Special:MyLanguage/Help: Watching pages|liste de suivi]] lorsqu'un lien vers une version d'un autre langage d'une page est ajouté, enlevé ou modifié sur Wikidata. Ce changement débutera le 5 octobre à 7h UTC en tant que test de 2 semaines pour réduire les notification à fort volume et de valeur basse ainsi que de rendre les modification importantes plus faciles à voir. Les utilisateurs qui se basent sur ces modifications peuvent en lire plus sur la [[m:Wikidata For Wikimedia Projects/Clearer Wikidata Edit Summaries/Hide Language Link edits|page du projet]] et partager du retour ou des préoccupations sur la [[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|page Discussion]] [https://phabricator.wikimedia.org/T439415] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:36|la tâche soumise|les {{formatnum:36}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:36||s}} la semaine dernière]]. Par exemple, un problème où les mentors qui se sont retirés du programme de mentorat pouvaient encore avoir des novices qui leur étaient affectés et recevaient des questions a été résolu. Les novices seront désormais réaffectés quand un mentor quittera le programme. [https://phabricator.wikimedia.org/T418194] '''Actualités pour la contribution technique''' * Wikimédia Brésil a publié son [[m:Special:MyLanguage/Wikimedia Brasil/Arcabouço de Boas Práticas de Desenvolvimento|Framework des Bonnes Pratiques de Développement]]. C'est un guide pour les staff, contracteurs et volontaires qui créent des outils et des projets techniques. Il couvre le cycle de vie du projet, la sécurité, l'accessibilité, la sécurité des données personnelles et l'utilisation responsable de l'IA, et explique comment des règles d'un projet ont été adaptées à un contexte local. D'autres affiliés sont encouragés à le réutiliser et à l'adapter. En lire plus dans le [[wmfblog:2026/09/26/wikimedia-brasils-development-best-practices-framework-technical-sustainability-as-an-organizational-responsibility|post Diff]]. * Une nouvelle base canonique d'URL pour les API Wikimedia sera bientôt introduite. Cette nouvelle route canonique suit le modèle <bdi lang="zxx" dir="ltr"><code><nowiki>{projectName}/api/</nowiki></code></bdi> et servira de voie alternative aux API REST actuelles (par exemple : <bdi lang="zxx" dir="ltr"><code><nowiki>{projectName}/w/rest.php/</nowiki></code></bdi>). Les deux versions seront appelées pour toutes les API existantes, mais la nouvelle structure sera le chemin préféré pour les modules API nouvellement créés. Ce changement ne devrait pas casser la plupart des flux de travail. Les URLs existantes resteront disponibles pour toutes les demandes de points finaux existants. Cependant, certains appels peuvent retourner des URL suivant le nouveau modèle dans le corps de la réponse. Les modifications seront d'abord disponibles sur Test Wiki, avec le déploiement prévu pour tous les wikis d'ici la fin octobre. [https://phabricator.wikimedia.org/T433535] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.23|MediaWiki]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/41|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W41"/> 5 octobre 2026 à 20:52 (CEST) <!-- Message envoyé par User:Trizek (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31098825 --> == <span lang="en" dir="ltr">Tech News: 2026-42</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W42"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/42|Translations]] are available. '''Weekly highlight''' * Starting October 13, wikis can change the default pattern for [[:en:Wikipedia:VisualEditor/Named references|auto-generated reference names in VisualEditor]] (currently <syntaxhighlight lang="wikitext" inline><ref name=":1" /></syntaxhighlight>). They will be able to use human-readable, content-based reference names that are more helpful to contributors using the source editor (e.g. <syntaxhighlight lang="wikitext" inline><ref name="Miller2026-1" /></syntaxhighlight>). VisualEditor reference name patterns can be defined using MediaWiki system messages, JSON, templates and modules ([[:mw:Special:MyLanguage/Help:Cite/VisualEditor reference names|more information]]). Admins can enable these changes via [[Special:CommunityConfiguration/Cite-VisualEditor-Autonames|community configuration]]. This is a [[:m:Special:MyLanguage/Community_Wishlist/W17|community wish]] implemented by [[:m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]]. [https://phabricator.wikimedia.org/T436255] * [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|Sub-referencing]] is a new MediaWiki feature that allows editors to reuse references with different details. It will be deployed to English Wikipedia on October 14 and to remaining Wikimedia Foundation wikis by end of October (likely October 21). The [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#deployment|FAQ]] lists possible actions to take on your wiki to support the deployment. This is a [[:m:WMDE Technical Wishes/Sub-referencing/History|long-standing community wish]] implemented by [[:m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]]. [https://phabricator.wikimedia.org/T440243][https://phabricator.wikimedia.org/T440304] '''Updates for editors''' * [[mw:Special:MyLanguage/Moderator Tools|Moderator Tools]] will be removing the list view of the Watchlist and defaulting everyone to the changelog view. Previously, there were two versions of how the Watchlist is shown to mobile web users: the list view, where editors could see watched articles in a list format, and the changelog view, where editors could see changes to watched articles in a log format, controlled by a user's Advanced Mobile Control settings. This has created issues for code-base maintenance: two different displays means two sets of code to manage. By defaulting to the changelog view, we are simplifying the codebase and avoiding the likelihood of missing bugs. * The Wikipedia Android app is testing a way to listen to article introductions in the “For you” feed. A text-to-speech model reads the existing article text aloud without rewriting it or adding any content. The 30-day experiment will begin in the Wikipedia Beta app during the third week of September, then move to the regular Wikipedia Android app in October for a limited group of eligible users. A small portion of app users (those with English feeds and at least one topic interest) will have access to the feature. This experiment is part of broader efforts to give app readers options in how they consume content in order to deepen engagement and user retention. [[mw:Special:MyLanguage/Readers/Listening to Wikipedia/Phase 1|Learn more]] and [[mw:Talk:Readers/Listening to Wikipedia|share feedback]]. * [[mw:Special:MyLanguage/Help:Extension:CodeMirror|CodeMirror syntax highlighting]] was [[phab:T288161|enabled]] by default for all logged out and newly registered users on the English Wikipedia on September 14. With no signs of adverse effects, the same change is being proposed to be made globally across all wikis in a few weeks. If you have concerns or feedback, or if your wiki would like to opt out, [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror#Enabling CodeMirror by default on all WMF wikis for newbies|please see the help page]]. [https://phabricator.wikimedia.org/T440077] * [[d:Wikidata:Usability_and_usefulness/Item_editing_experience/Mobile_editing_of_statements|Wikidata mobile statement editing]] of all datatypes will be officially launched on October 13, when it will be removed from Wikidata [[d:Special:Preferences#mw-prefsection-betafeatures|beta features]] and become available to all editors. Users can [[d:Wikidata_talk:Usability_and_usefulness/Item_editing_experience/Mobile_editing_of_statements|report bugs and share feedback]]. * Last week, language link changes were removed from [[Special:RecentChanges|Recent Changes]] and [[Special:Watchlist|Watchlist]] feeds on Wikivoyage, Wikisource, Wikiversity, Wikiquote and Wiktionary. As no adverse effects have been observed, the [[m:Wikidata_For_Wikimedia_Projects|Wikidata For Wikimedia Projects]] plans to roll out this change globally to all Wikipedias. This is scheduled for October 13 at 07:00 UTC, after which editors will no longer see when a link to ''another'' language version of a page is added, removed, or changed in its Wikidata item. Editors who rely on these notifications can read more on the [[m:Wikidata For Wikimedia Projects/Clearer Wikidata Edit Summaries/Hide Language Link edits|project page]] and share feedback or ask to opt out their wiki on the [[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|talk page]]. [https://phabricator.wikimedia.org/T440698] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where accidentally touching the graph while scrolling on the Pageviews tool on mobile could unintentionally zoom in has now been fixed. Click-and-drag zoom is now disabled on touch devices. [https://phabricator.wikimedia.org/T440075] '''Updates for technical contributors''' * There are three major changes coming to dumps: the legacy content dumps [https://lists.wikimedia.org/hyperkitty/list/xmldatadumps-l@lists.wikimedia.org/thread/E6D5EU4PMSTSOI2J7A46HJ3YW2W554CS/ deprecated in January 2026] will now be sunset, the replacement [https://wikitech.wikimedia.org/wiki/MediaWiki_Content_File_Exports MediaWiki Content File Exports] will be delayed to implement a “safety buffer”, and they are now also available as 7zip-compressed files. Users will need to switch the URL they download their dump from. For more information, timeline and migration instructions, see [https://wikitech.wikimedia.org/wiki/Legacy_content_dumps_sunset Legacy content dumps sunset]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.24|MediaWiki]] '''In depth''' * The Wikimedia Foundation discovered unauthorized activity by OpenAI agents on Wikimedia projects, including edits to wikis, attempts to exploit a public note-taking tool, and heavy automated traffic. [[diffblog:2026/10/05/openai-rogue-agent-activities-found-on-wikimedia-projects/|Read the blog post on Diff]] for more details about the activity and the Foundation’s response. '''Meetings and events''' * Save the date for the next Wikimedia Hackathon! We’ll be meeting in lovely Tirana, Albania for the [https://www.mediawiki.org/wiki/Wikimedia%20Hackathon%202027 2027 Wikimedia Hackathon] from April 9–11, 2027. Scholarship and staff sponsorship applications are currently being [https://pretix.eu/wikimedia/Hackathon-2027/ accepted through Pretix] and will close on October 30, 2026. General registration will open in January 2027. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/42|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W42"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 13 octobre 2026 à 09:39 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31142468 --> 4mz9chnt7rd05pa2aq79v7qhc9u7dvi Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO 0 83778 775122 772104 2026-10-13T02:22:17Z Mewtow 31375 /* Le Motorola 68881 et le 68882 */ 775122 wikitext text/x-wiki Les '''coprocesseurs''' sont des processeurs secondaires qui complémentent un processeur principal. Ils permettent de déléguer certains calculs à un processeur secondaire, afin de décharger le processeur principal. Un détail important est que le processeur principal et le coprocesseur sont très différents : ils n'ont pas le même jeu d'instruction, n'ont pas les mêmes performances, et bien d'autres différences. Nous en avions déjà vu dans le chapitre sur l'architecture de base, où nous avions analysé d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail ==Les différents types de coprocesseurs== Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties. Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80. Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici. [[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]] Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants. ===Les coprocesseurs faiblement et fortement couplés=== À ce stade du cours, nous allons distinguer les coprocesseurs faiblement couplés et fortement couplés. La distinction est que les premiers sont assez détachés du processeur, alors que les seconds ne le sont pas. La distinction n'est pas très claire, aussi j'ai décidé de prendre cette définition : les premiers sont traités comme des entrées-sorties, alors que les seconds sont intégrés sur le bus mémoire. Avec les '''coprocesseurs fortement couplés''', le CPU et le coprocesseur sont connectés sur le même bus mémoire. Le programme exécuté contient des instructions à destination du CPU, d'autres à destination du coprocesseur, dont l'encodage est différent. Les deux surveillent le bus mémoire et décident à qui est destinée l'instruction. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. Pour cela, le CPU envoie son ''program counter'' sur le bus d'adresse, ce qui entraine l'apparition d'une instruction sur le bus de données. Là, le processeur et le coprocesseur reçoivent l'instruction et la décodent. Si l'instruction est destinée au processeur, le CPU l'exécutera, alors que le coprocesseur la traitera comme un NOP. Et inversement si l'instruction est destinée au coprocesseur. Les '''coprocesseurs faiblement couplés''' sont des entrées-sorties mappées en mémoire, avec des registres d'interfaçage. Le processeur écrit une instruction et ses opérandes dans ces registres d'interfaçage, et le coprocesseur fait les calculs dans son coin. Le processeur récupère le résultat quelques cycles plus tard, en le lisant dans un autre registre d’interfaçage. La récupération du résultat peut se faire avec du ''pooling'' ou des ''interruptions inter-processeurs''. Le coprocesseur peut envoyer une interruption au processeur principal pour dire qu'il a terminé son travail. Parfois, les interruptions peuvent aller dans l'autre sens. Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80. Les coprocesseurs sonores sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore. Et ce n'est possible que si ces coprocesseurs sont faiblement couplés. Mais quelques coprocesseurs arithmétiques sont dans ce cas aussi, comme on le verra plus bas. [[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]] Il existe cependant des cas assez difficiles à classer. Nous verrons le cas des Motorola 68881 dans ce qui suit, qui sont un mélange des deux solutions. ===Les coprocesseurs arithmétiques : quelques généralités=== Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques. Et ce pour une raison très simple : nous avons déjà vu les coprocesseurs I/O et les coprocesseurs sonores dans un chapitre antérieur. Pour rappel, les coprocesseurs sonores sont des cartes sons, ou du moins une partie de carte son. De plus, on a plus de documentation sur les coprocesseurs arithmétiques. Il faut dire que c'étaient des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants Ils étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel. Le problème ne se pose pas sur les consoles de jeu, mais il est assez rare que les consoles de jeu incorporent des coprocesseurs arithmétiques. Il existe cependant des exceptions. Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre. ==Les coprocesseurs pour les CPU x86== Dans le chapitre précédent, nous avons parlé du jeu d'instruction x87, qui ajoutait le support des nombres flottants aux PC x86. Formellement, le x87 est apparu avec le coprocesseur 8087, un coprocesseur Intel prévu pour être utilisé avec un 8086. Mais ce n'est pas le premier coprocesseur flottant pour PC. Il a existé quelques coprocesseurs avant lui, qui ne respectaient pas le standard x87. Et ne parlons pas des coprocesseurs Motorola et autres, qui suivaient un autre standard et ne fonctionnaient pas avec un CPU x86. Dans cette section, nous allons voir les coprocesseurs conçus pour les PC, qui fonctionnaient en tandem avec un CPU x86. Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres. ===Les précurseurs : l'Intel 8231 et le 8232=== L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas. L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division). : La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée. [[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]] Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit. Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile. Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié. {|class="wikitable" |- ! A0, RD, WR !! Action |- | 000 || Lecture de l'octet depuis les registres de données. |- | 010 || Écriture de l'octet dans les registres de données. |- | 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement. |- | 101 || Lecture du registre d'état. |} Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232. * READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut. * END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0. * EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0. Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non. Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle ! Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz. ===L'intel 8087 et ses successeurs=== [[File:Intel 8087.svg|vignette|Intel 8087]] Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée. L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL. Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel. Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage. Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande. Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation. Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend. Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants. Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant. [[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]] Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser). De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe. L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins. [[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]] L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul. Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs. Pour finir, voici quelques liens sur la microarchitecture du 8087 : * [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip]. * [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter]. * [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered]. * [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode]. * [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die]. * [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip]. ===Les coprocesseurs x87 de Weitek=== Intel n'a pas été le seul fabricant à commercialiser des coprocesseurs x87. Weitek et de nombreuses autres entreprises s'y sont mises. Weitek a commercialisé plusieurs coprocesseurs : un premier coprocesseur appelé le 1067, le 1167, le 2167, le 3167 et le 4167. Ils sont tous rétrocompatibles entre eux, à savoir que le 4167 ne fait qu'ajouter des fonctionnalités au 3167, qui lui-même ajoute des fonctionnalités au 2167, et ainsi de suite. Ils gèrent tous les quatre opérations de base, ainsi que le calcul de la racine carrée. Le '''Weitek 1067''' avait pour particularité d'être fourni en pièces détachées, avec trois circuits séparés : un circuit de contrôle, une ALU flottante, et un multiplieur/diviseur. Ces trois pièces détachées étaient censées être soudées sur la carte mère. Le '''Weitek 1167''' regroupait ces trois pièces détachées sur une carte d'extension ISA. Par la suite, le '''Weitek 2167''' regroupa les trois pièces détachées dans un seul circuit imprimé. Il n'y avait pas de registres flottants adressables, ni de pile de registres. À la place, l'ALU et le multiplieur/diviseur intégraient deux registres pour les opérandes et un registre accumulateur. Les opérandes étaient présentés sur le bus de données et l'ALU les mémorisait dans deux registres internes de 64 bits chacun, nommés A et B. Le coprocesseur lisait les opérandes depuis ces deux registres et mémorisait le résultat dans un registre de résultat interne. Il envoyait alors le résultat sur le bus de données, prévenait le CPU avec une interruption, et le CPU récupérait le résultat sur le bus. Il faut noter que toutes les communications avec l'ALU passent par le bus de données, appelé le bus X. La transmission d'un flottant 32 bits se faisait en un cycle d'horloge, vu que le bus était de 32 bits. Par contre, la transmission d'un flottant 64 bits se faisait en deux cycles. À part le bus de données, il y avait un bus de commande, relié à l'unité de contrôle 1163. Elle envoyait l'opcode sur une entrée dédiée, notée F. Les bits de commande L, CSL, CSUS, CUSX, U commandent la lecture des opérandes ou leur écriture. L'unité de contrôle recevait le registre d'état via une sortie dédiée nommée S ou STATUS. [[File:Weitek WTL1167 arch.svg|centre|vignette|upright=2.5|Weitek WTL1167.]] Pour charger les opérandes dans l'ALU, celle-ci intégrait diverses entrées de commande nommées L0, L1, L2, L3 et CSL. * Le signal CSL est le ''Chip Select Load'', ce qui indique qu'il est mis à 1 lors d'une lecture. * Le bit L0 à 1 indique qu'un opcode est envoyé sur l'entrée F, l'entrée pour l'opcode est recopiée dans un registre interne, qui n'est pas un registre d'instruction vu que l'ALU n'a pas de décodeur. * Les bits L1 et L2 indiquent si ce qu'il y a sur le bus est : un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. * Le registre L3 indique quel est le registre de destination : A ou B. Les signaux U, CSUS et CSUX servaient pour l'envoi du résultat sur le bus. Ils précisaient s'il fallait copier un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. Le '''Weitek 3167''' était conçu pour fonctionner en tandem avec un CPU Intel 386, alors que le '''Weitek 4167''' était prévu pour aller avec un Intel 486, mais ils fonctionnaient de la même manière au-delà de quelques détails. Contrairement à leurs prédécesseurs, ils intégraient 32 registres flottants de 32 bits. Un registre pair et un registre impair pouvaient être concaténés pour mémoriser un opérande 64 bits. Les opérations se faisaient soit entre deux registres flottants, soit entre un registre flottant et l'opérande présentée sur le bus de données. Le résultat est stocké dans un registre flottant. Leur registre de status contenait les résultats des comparaisons flottantes, ainsi que 8 bits pour les exceptions flottantes. Le registre de contrôle avait un champ pour configurer les arrondis, mais aussi un masque d'exceptions de 8 bits disant quels bits d'exceptions ignorer dans le registre de statut. Le coprocesseur est mappé en mémoire, ce qui fait qu'il a des adresses réservées dans lesquelles le processeur peut lire/écrire, pour lui envoyer une instruction, envoyer un opérande ou récupérer un résultat. Les adresses en question sont les adresses COOO OOOOh à COOO FFFFh, ce qui fait que le coprocesseur est adressé si les 16 bits de poids fort de l'adresse valent COOO. Par contre, les 65536 adresses réservées n'étaient pas associées à de la mémoire, pas même aux registres. A la place, les 16 bits de poids faible de l'adresse encodaient une instruction au coprocesseur, à savoir un opcode de 6 bits deux numéros de registres. Le processeur communique avec le coprocesseur en envoyant l'instruction sur le bus d'adresse, et éventuellement un opérande sur le bus de données. L'opérande vient généralement des registres, par simplicité, car cela permet de tout envoyer en une seule écriture. Par exemple, une écriture du registre EAX à l'adresse COOO OOOOh va copier le registre EAX sur le bus de données, et envoyer l'opcode de l'addition (0000) sur le bus d'adresse avec deux numéros de registre. Le coprocesseur fait alors une addition entre le registre flottant sélectionné, et l'opérande sur le bus de données copiée depuis EAX. ==Le Motorola 68881 et le 68882== Le 68881 de Motorola était conçu pour fonctionner avec les CPU 68020 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Pour une instruction coprocesseur, il lisait les opérandes en RAM, puis envoyait instruction et opérandes au 68881. Il continuait son travail dans son coin, et récupérait le résultat quelques cycles plus tard. Malgré le fait qu'il y ait des instructions destinées au coprocesseur, le 68881 n'était pas un coprocesseur fortement couplé. A la place, le 68881 était géré comme une entrée-sortie, du point de vue du CPU. Il était mappé en mémoire, avait une entrée ''Chip Select'' commandé par décodage d'adresse. Il contenait aussi des registres d'interface, appelés des ''coprocessor interface registers'' (CIRs). Il y avait des registres pour l'opcode de l'instruction à exécuter, un autre pour chaque opérande, etc. Pour envoyer une instruction au 68881, le CPU avait juste à écrire dans les registres adéquats, idem pour charger les opérandes de l'instruction si besoin. Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Fait étonnant, cela ressemble beaucoup à ce qui est fait avec les coprocesseurs x87 : usage de flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, le jeu d'instruction est complétement différent. Les coprocesseurs Motorola avaient aussi un registre de statut et un registre de contrôle, guère plus. Le registre de statut mémorisait les conditions classiques, mais aussi des bits pour les exceptions qui ont été levées lors d'un calcul. Le registre de contrôle mémorisait de quoi configurer les arrondis, mais aussi un masque pour indiquer quelles exceptions ignorer dans le registre de statut. Pareil que pour les processeurs précédents, donc. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les sections critiques et le modèle mémoire | prevText=Les sections critiques et le modèle mémoire | next=Les ISA optimisés pour la compilation/interprétation | nextText=Les ISA optimisés pour la compilation/interprétation }} </noinclude> 7m8j8bg1ar707x61etx7kneeajhduoo Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations 0 84538 775093 774786 2026-10-12T21:55:42Z Mewtow 31375 /* Le découplage décodage-renommage */ 775093 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 ''scheduler'', 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 ''scheduler'' 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''. [[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]] 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. 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. L'implémentation peut utiliser la ''lecture après émission'' ou ''avant émission''. Dans les deux cas, les registres entiers et flottants sont lus en parallèle. Ainsi, si une µop entière et une µop flottante sont émises en même temps, les opérandes de la µop entière sont lues depuis le banc de registres généraux, les opérandes de la µop flottante sont lus depuis les registres flottants, les deux lectures se font en même temps. Par contre, cela a un impact sur le ''scheduler''. Avec la ''lecture avant émission'', il doit avoir assez de ports pour insérer 4 opérandes dans deux µops. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture après émission.]] [[File:Read Before Issue.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture avant émission.]] 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 calculs d'adresse=== Une autre scission, aussi naturelle que la précédente, serait d'utiliser un ''scheduler'' spécialisé pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Mais cela ne marcherait pas, pour une raison que vous ne deviez pas oublier : les µops mémoire doivent se faire dans l'ordre du programme, sauf à utiliser des techniques de désambiguïsation mémoire. Par contre, il est possible de séparer les calculs d'adresse des autres opérations entières. Dans ce qui suit, on port du principe que les µops mémoire sont scindées en deux : une µop de calcul d'adresse, et une µops mémoire. La première calcule l'adresse utilisée par la seconde, la seconde exécute l'accès mémoire dans le cache. La séparation permet d'exécuter les calculs d'adresse dans le désordre, mais les µops mémoire se font elles dans l'ordre. Le transfert de l'adresse calculée peut se faire de plusieurs manières, mais nous en parlerons en détail dans le chapitre sur la désambiguïsation mémoire. Pour le moment, on suppose qu'elle se fait à travers un ''registre d'adresse'' "caché", dans le sens où il n'est pas un registre architectural nommé. La µop de calcul d'adresse enregistre son résultat dans ce registre, la µop mémoire lit ce registre après émission. Le registre d'adresse est souvent un registre physique, que l'unité de renommage attribue comme registre de destination du calcul d'adresse. L'avantage est que les calculs d'adresse sont fait plus tôt, qu'avec un ''scheduler'' centralisé. Par exemple, on ne se retrouve plus dans une situation où plusieurs µops entières passer devant une µop de calcul d'adresse. Les µops entières et de calcul d'adresse sont prêtes, mais les µops entières sont exécutées en premier, la µop de calcul d'adresse est mise en attente pendant quelques cycles. Et non seulement les calculs d'adresse sont émis au plus tôt, mais ils ont de plus des ports de lecture dédié sur le banc de registres. Et effectuer les calculs d'adresse rapidement est primordial, car cela permet de connaitre à l'avance les adresses, donc d’effectuer les lectures le plus tôt possible. ===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 ''scheduler'' flottante 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 ''scheduler'' 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''. ==La complétion dans le désordre== L'exécution dans le désordre implique la présence d'un ROB, ou d'une structure similaire. Sans cela, l'exécution dans le désordre aurait des exceptions imprécises, ce qui n'est pas acceptable sur les processeurs commerciaux actuels. Le ROB remet dans l'ordre les écritures dans les registres architecturaux, afin de gérer les branchements et exceptions. Il accumule pour cela les instructions exécutées en avance, pour retarder leurs écritures, afin qu'elles se fassent au bon moment. Mais de rares optimisations permettent de désactiver cette remise en ordre dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. Elles s'activent en absence de branchements et quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les isntructions quittent le ROB en avance. Le résultat est que écritures se font dans le désordre, dans le ''retirement register file'', ou le banc de registres physique. L'avantage est que les instructions quittent le ROB plus rapidement que prévu, ce qui utilise sa capacité au mieux. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le renommage de registres | prevText=Le renommage de registres | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> a0ol0fvzsokczkazvqvubv54kfdo3en 775095 775093 2026-10-12T21:56:46Z Mewtow 31375 /* La complétion dans le désordre */ 775095 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 ''scheduler'', 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 ''scheduler'' 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''. [[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]] 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. 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. L'implémentation peut utiliser la ''lecture après émission'' ou ''avant émission''. Dans les deux cas, les registres entiers et flottants sont lus en parallèle. Ainsi, si une µop entière et une µop flottante sont émises en même temps, les opérandes de la µop entière sont lues depuis le banc de registres généraux, les opérandes de la µop flottante sont lus depuis les registres flottants, les deux lectures se font en même temps. Par contre, cela a un impact sur le ''scheduler''. Avec la ''lecture avant émission'', il doit avoir assez de ports pour insérer 4 opérandes dans deux µops. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture après émission.]] [[File:Read Before Issue.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture avant émission.]] 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 calculs d'adresse=== Une autre scission, aussi naturelle que la précédente, serait d'utiliser un ''scheduler'' spécialisé pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Mais cela ne marcherait pas, pour une raison que vous ne deviez pas oublier : les µops mémoire doivent se faire dans l'ordre du programme, sauf à utiliser des techniques de désambiguïsation mémoire. Par contre, il est possible de séparer les calculs d'adresse des autres opérations entières. Dans ce qui suit, on port du principe que les µops mémoire sont scindées en deux : une µop de calcul d'adresse, et une µops mémoire. La première calcule l'adresse utilisée par la seconde, la seconde exécute l'accès mémoire dans le cache. La séparation permet d'exécuter les calculs d'adresse dans le désordre, mais les µops mémoire se font elles dans l'ordre. Le transfert de l'adresse calculée peut se faire de plusieurs manières, mais nous en parlerons en détail dans le chapitre sur la désambiguïsation mémoire. Pour le moment, on suppose qu'elle se fait à travers un ''registre d'adresse'' "caché", dans le sens où il n'est pas un registre architectural nommé. La µop de calcul d'adresse enregistre son résultat dans ce registre, la µop mémoire lit ce registre après émission. Le registre d'adresse est souvent un registre physique, que l'unité de renommage attribue comme registre de destination du calcul d'adresse. L'avantage est que les calculs d'adresse sont fait plus tôt, qu'avec un ''scheduler'' centralisé. Par exemple, on ne se retrouve plus dans une situation où plusieurs µops entières passer devant une µop de calcul d'adresse. Les µops entières et de calcul d'adresse sont prêtes, mais les µops entières sont exécutées en premier, la µop de calcul d'adresse est mise en attente pendant quelques cycles. Et non seulement les calculs d'adresse sont émis au plus tôt, mais ils ont de plus des ports de lecture dédié sur le banc de registres. Et effectuer les calculs d'adresse rapidement est primordial, car cela permet de connaitre à l'avance les adresses, donc d’effectuer les lectures le plus tôt possible. ===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 ''scheduler'' flottante 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 ''scheduler'' 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''. ==La complétion dans le désordre== L'exécution dans le désordre implique la présence d'un ROB, ou d'une structure similaire. Sans cela, l'exécution dans le désordre aurait des exceptions imprécises, ce qui n'est pas acceptable sur les processeurs commerciaux actuels. Le ROB remet dans l'ordre les écritures dans les registres architecturaux, afin de gérer les branchements et exceptions. Il accumule pour cela les instructions exécutées en avance, pour retarder leurs écritures, afin qu'elles se fassent au bon moment. Mais de rares optimisations permettent de désactiver cette remise en ordre dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''. Elles s'activent en absence de branchements et quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les isntructions quittent le ROB en avance. Le résultat est que écritures se font dans le désordre, dans le ''retirement register file'', ou le banc de registres physique. Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il est activé quand une lecture fait un défaut de cache. Le défaut de cache peut parfois lever une exception matérielle, par exemple s'il entraine un défaut de page, ou une erreur de protection mémoire. En théorie, les instructions après la lecture doivent accumuler leurs résultats dans le ROB, en attendant que le défaut de cache soit traité. C'est seulement une fois le défaut de cache terminé que les instructions suivantes peuvent ''retire'', sortir du ROB. L'optimisation du Cortex A73 est que le processeur détecte les exceptions en avance, en accédant rapidement à la TLB. Une fois que le processeur sait qu'aucune exception n'aura lieu, les instructions en attente peuvent ''retire'' immédiatement, et écrire dans les registres architecturaux. Et ce alors que le défaut de cache n'est pas terminée. Par contre, le mécanisme ne s'applique qu'en absence de branchements, pour ne pas autoriser des écritures dans les registres par des instructions peut-être exécutées à tord. Le mécanisme est aussi désactivé sir la lecture est exécutée de manière spéculative, si son exécution l'a été çà la suite d'une prédiction de branchement non-résolue. L'idée est donc de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en plein défaut de cache. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon. L'avantage est que les instructions quittent le ROB plus rapidement que prévu, ce qui utilise sa capacité au mieux. Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le renommage de registres | prevText=Le renommage de registres | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> py3dach8hybt2kar6yhicomd2dlmvon 775112 775095 2026-10-12T23:44:34Z Mewtow 31375 /* La complétion dans le désordre */ 775112 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 ''scheduler'', 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 ''scheduler'' 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''. [[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]] 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. 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. L'implémentation peut utiliser la ''lecture après émission'' ou ''avant émission''. Dans les deux cas, les registres entiers et flottants sont lus en parallèle. Ainsi, si une µop entière et une µop flottante sont émises en même temps, les opérandes de la µop entière sont lues depuis le banc de registres généraux, les opérandes de la µop flottante sont lus depuis les registres flottants, les deux lectures se font en même temps. Par contre, cela a un impact sur le ''scheduler''. Avec la ''lecture avant émission'', il doit avoir assez de ports pour insérer 4 opérandes dans deux µops. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture après émission.]] [[File:Read Before Issue.png|centre|vignette|upright=2.5|Processeur avec un ''scheduler'' flottant, avec lecture avant émission.]] 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 calculs d'adresse=== Une autre scission, aussi naturelle que la précédente, serait d'utiliser un ''scheduler'' spécialisé pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Mais cela ne marcherait pas, pour une raison que vous ne deviez pas oublier : les µops mémoire doivent se faire dans l'ordre du programme, sauf à utiliser des techniques de désambiguïsation mémoire. Par contre, il est possible de séparer les calculs d'adresse des autres opérations entières. Dans ce qui suit, on port du principe que les µops mémoire sont scindées en deux : une µop de calcul d'adresse, et une µops mémoire. La première calcule l'adresse utilisée par la seconde, la seconde exécute l'accès mémoire dans le cache. La séparation permet d'exécuter les calculs d'adresse dans le désordre, mais les µops mémoire se font elles dans l'ordre. Le transfert de l'adresse calculée peut se faire de plusieurs manières, mais nous en parlerons en détail dans le chapitre sur la désambiguïsation mémoire. Pour le moment, on suppose qu'elle se fait à travers un ''registre d'adresse'' "caché", dans le sens où il n'est pas un registre architectural nommé. La µop de calcul d'adresse enregistre son résultat dans ce registre, la µop mémoire lit ce registre après émission. Le registre d'adresse est souvent un registre physique, que l'unité de renommage attribue comme registre de destination du calcul d'adresse. L'avantage est que les calculs d'adresse sont fait plus tôt, qu'avec un ''scheduler'' centralisé. Par exemple, on ne se retrouve plus dans une situation où plusieurs µops entières passer devant une µop de calcul d'adresse. Les µops entières et de calcul d'adresse sont prêtes, mais les µops entières sont exécutées en premier, la µop de calcul d'adresse est mise en attente pendant quelques cycles. Et non seulement les calculs d'adresse sont émis au plus tôt, mais ils ont de plus des ports de lecture dédié sur le banc de registres. Et effectuer les calculs d'adresse rapidement est primordial, car cela permet de connaitre à l'avance les adresses, donc d’effectuer les lectures le plus tôt possible. ===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 ''scheduler'' flottante 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 ''scheduler'' 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 renommage de registres | prevText=Le renommage de registres | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> cu2veka7w8d1z0v024za3ymqiig5mvf Mathc initiation/007n 0 84562 775040 774749 2026-10-12T13:01:45Z Xhungab 23827 775040 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/a594#Se familiariser avec les series de Fourier :| Sommaire]] : : ==La fonction : f(t) = t^2 .................. avec t = [-PI..PI]== : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]] : . : ==La fonction f :== * [[Mathc initiation/007h|f2.h ]] ...... '''f(t) = t^2 avec t = [-PI..PI]''' : . : ==Les coefficients de la séries de Fourier :== * [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> . {| class="wikitable" |+ Séries de Fourier |- ! plot[-pi:pi][-1.:10] !! plot[-pi:4*pi][-1.:10] |- | [[File:Fourier serie for my wikibook 2.png|thumb|]] || [[File:Fourier serie for my wikibook 3.png|thumb|]] |} Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les 9 premiers termes''' de la série. * [[Mathc initiation/007l|c2fs.c]] Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série. * [[Mathc initiation/007m|c2fsN.c]] ==Les spectres d'amplitudes et les spectres de phases== * [[Mathc initiation/007q|c2fsap.c]] {{AutoCat}} id6qfksyjg95y9dk2qzhb3ml94q1gv1 Fonctionnement d'un ordinateur/Les pipelines in-order 0 84572 775059 775020 2026-10-12T16:51:21Z Mewtow 31375 /* Les défauts de cache bloquent le pipeline */ 775059 wikitext text/x-wiki Avec ce qu'on a dit dans le chapitre précédent, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipeline doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on va expliquer dans ce chapitre. Dans ce chapitre, nous allons voir les '''processeurs ''in-order'''''. Les définir n'est pas facile, surtout à ce stade du cours. Ils sont opposés aux processeurs avec exécution dans le désordre, qu'on verra dans quelques chapitres. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. On démarre les instructions une par une, dans un certain ordre. Mais le parcours du pipeline n'est pas un long fleuve tranquille, comme on le verra dans ce chapitre, et le résultat à la fin du pipeline est parfois différent. À la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Au final, deux instructions peuvent théoriquement arriver dans le désordre. Du moins, ce serait le cas si le processeur n'était pas conçu pour corriger le problème. Les processeurs ''in-order'' garantissent que des instructions chargées dans un certain ordre termineront dans le même ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner au mieux. Les ''processeurs à exécution dans le désordre'', eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance. Mais ils ont des systèmes pour éviter que cela pose problème. Ils utilisent pour cela des systèmes d'exception précises plus élaborés que ceux des chapitres précédents. Mais cela sera pour un autre chapitre. Pour ce chapitre, nous allons nous concentrer sur les processeurs ''in-order''. Mais avant toute chose, un petit rappel. Un processeur avec un pipeline est décomposé en deux parties. Le ''front end''), prend en charge les étages communs à toutes les instructions, comme le chargement et le décodage. L'amont est suivi une unité d'émission, elle-même suivie par le chemin de données. Le chemin de données contient plusieurs unités différentes : des unités de calcul, l'unité mémoire, parfois des unités pour les branchements. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] Avec des instructions multi-cycles, le pipeline exploite plusieurs unités en même temps, c'est même la norme. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. Et le reste du chapitre vise à expliquer quelles sont ces contraintes. ==Les défauts de cache bloquent le pipeline== Une instruction mémoire a un temps d'exécution variable, en raison des défauts de cache. En général, elle fait un succès de cache et accède au cache L1 de données. L'accès mémoire se fait en un cycle, parfois 2 cycles si le cache est pipeliné. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : une dizaine de cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation. Et si le défaut de cache se double d'un défaut de page mémoire, l'instruction peut dépasser le millier de cycles d'horloge. Les pipelines précédents doivent donc gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Un problème est que les accès mémoire ne peuvent pas être gérés comme les autres instructions multi-cycles, car ce sont les seules instructions au temps d'exécution variable, et imprédictible. La solution utilisée par tous les processeurs dits ''in-order'', suppose que les instructions mémoire font un succès de cache et s'exécutent en un cycle d'horloge. Si un défaut de cache a lieu, on bloque le pipeline. ===Le blocage du pipeline par l'unité mémoire=== Pour geler le pipeline, l'unité mémoire génère un signal ''pipeline stall''. Il est mis à 1 pendant qu'un défaut de cache est en cours, il est remis à zéro une fois le défaut de cache terminé. Ce signal est utilisé pour geler les registres du pipeline, dans les étages précédents. Seuls les étages situés avant l'étage MEM sont gelés. Ceux situés après peuvent faire leur travail normalement, ce qui permet au système d'exceptions précises de faire son travail. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé. [[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]] La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de cache est en cours. C'est seulement quand le défaut est terminé et qu'il reçoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline. ===Pourquoi bloquer le pipeline en cas de défaut de cache ?=== Mais pourquoi bloquer le pipeline en cas de défaut de cache ? Par exemple, si une lecture déclenche un défaut de cache, pourquoi ne pas continuer à émettre les instructions après la lecture ? En théorie, c'est possible, c'est même ce que font les processeurs à exécution dans le désordre. Ils ne sont pas bloqués après un défaut de cache. Cependant, ils peuvent le faire car ils ont un système d'exception précise élaboré, pas les processeurs ''in-order''. Si les instructions après la lecture continuaient, elles finirait par atteindre la fin du pipeline, voire par le quitter. Un défaut de cache dure longtemps, une dizaine de cycles, voire plus. C'est souvent plus de cycles que la totalité du pipeline du processeur. Et encore plus que le nombre de cycles avant l'unité mémoire. Le résultat est que ces instructions quitteraient le pipeline, écriraient dans les registres, voire dans la mémoire RAM, et ce serait définitif. Si la lecture déclenche une exception matérielle, on ne pourrait pas revenir en arrière, le système d'exception précise serait en erreur. Néanmoins, cela suggère une optimisation. L'idée est de continuer à exécuter des instructions après le défaut de cache. L'unité mémoire est bloquée, elle ne peut pas accepter de nouvelle instruction, mais l'ALU entière, la FPU et les autres circuits sont utilisables. Les instructions émises après le défaut de cache sont alors appelées les ''instructions en avance''. Elles n'ont pas le droit d'enregistrer leurs résultats, que ce soit dans les registres ou la RAM. Pour cela, les micro-opérations restent dans le pipeline et sont stoppées dans le dernier étage libre. Par exemple, imaginons qu'un défaut de cache ait lieu à l'instant T, au cycle T. La lecture va se propager dans le pipeline jusqu'à l'étage de ''writeback'', dont à la toute fin du pipeline. La micro-opération suivante, émise au cycle T+2, s’arrêtera à l'étage d'avant. La micro-opération encore avant s’arrêtera à l'étage encore avant, et ainsi de suite. Le pipeline finira par se remplir totalement, et l'unité d'émission n'aura pas le choix que de bloquer le pipeline. Le résultat est que des micro-opérations auront été préchargées et exécutées à l'avance, mais ne se seront pas "terminées". Il y a toujours la possibilité de les annuler, en cas de problèmes. Le nombre de micro-opérations chargées en avance est égal au nombre d'étage du pipeline, vu qu'il est totalement remplit. Cette optimisation a possiblement été utilisée par les processeurs ARM de marque Cortex A53, A55 et A510, bien que ce ne soit pas certain (il s'agit d'une supposition fite par l'auteur du bloc "Chips and cheese". Le Cortex A52 et le A55 pouvaient exécuter en avance 8 instructions, et ils avaient un pipeline de 8 étages, alors que le A510 pouvait avoir 12 instructions en avance, et il avait justement un pipeline de 12 étages. {|class="wikitable" |- ! Fetch 1 !! Fetch 2 !! Décodage 1 !! Décodage 2 !! Émission !! Lecture des registres !! Exécution !! ''Writeback'' |- | Micro-opération T + 8 || Micro-opération T + 7 || Micro-opération T + 6 || Micro-opération T + 5 || Micro-opération T + 4 || Micro-opération T + 3 || Micro-opération T + 2 || Micro-opération T + 1 || colspan="8" class="f_rouge" | Lecture, en défaut au cycle T |} ==L'inversion de l'ordre des écritures== Démarrer une nouvelle instruction par cycle est la base du pipeline. Mais avec des instructions multi-cycles, un problème bien particulier survient. Le processeur exécute plusieurs instructions en même temps, dans le même étage d’exécution, mais dans des ALUS/multiplieurs/FPU séparés. Le problème, c'est que si les instructions démarrent dans l'ordre du programme, les unes après les autres, elles peuvent terminer dans le désordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé ! Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre. On dit qu'il y a '''inversion de l'ordre des écritures'''. Et cette inversion cause de nombreux problèmes. Premier problème : des registres peuvent avoir une valeur invalide. Si les écritures changent d'ordre, une instruction peut lire un opérande qui n'est pas la bonne, qui n'a pas été écrite par la bonne instruction. Le programme ne s'exécute pas correctement. De plus, les mécanismes d'exception précises cessent de fonctionner, car ils supposent que les écritures dans les registres se font dans l'ordre. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Maintenant, autre problème, qui peut être vu comme un cas particulier du précédent. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Il y a un phénomène de '''double écriture simultanées'''. Et s'il s'agit là d'un exemple qui mélange dépendance structurelle et dépendance WAW. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! ... |- | Multiplication || Multiplication || Multiplication ||class="f_bleu" | Écriture || || |- | || Addition || Addition || class="f_bleu" | Ecriture || |} Il y a deux solutions principales pour régler ce problème, avec une troisième solution spécialement adaptée aux instructions mémoire, qui sont à part des autres. La première méthode permet d'utiliser des instructions multi-cycles, avec un pipeline fixe. La seconde utilise soit un pipeline fixe, soit un pipeline dynamique, mais avec des restrictions assez importantes. La gestion des défauts de cache est elle un cas à part, valable sur tous les types de pipelines. ===Les registres d'échelonnement=== La première solution est assez brute de décoffrage. Elle force toutes les instructions à prendre exactement le même nombre de cycles d'horloge. Elle utilise donc un pipeline où toutes les instructions ont le même nombre d'étages. Un tel pipeline est appelé un ''pipeline de longueur fixe'', ou encore un '''pipeline fixe'''. Et le processeur se cale sur les instructions les plus lentes. Par exemple, si une addition prend un cycle d'horloge, qu'une multiplication en prend 8 et qu'un décalage en prend 2, alors toutes les instructions prendront 8 cycles d'horloge. Et ces 8 cycles d'horloge correspondront à 8 étages d'exécution. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] Un défaut de la méthode précédente est que les données sont copiées d'un registre de pipeline au suivant à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs ''in order'' modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé. [[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]] ===Les blocages de pipeline=== Une solution alternative n'utilise ni registres d'échelonnage, ni étages qui ne font rien. Elle utilise à la place un '''pipeline dynamique''', de taille variable selon l'instruction. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. Les instructions multi-cycles prennent le temps qu'il faut pour faire leur calcul, les additions donnent leur résultat en un seul cycle, bref : les instructions ont des temps d'exécution variables. On ne se cale pas sur la plus lente. De plus, les instructions écrivent dans les registres au plus tôt, dès qu'elles ont fini leur calcul. [[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]] Pour éviter d'inverser les écritures, ce sont les instructions qui sont retardées. Pour comprendre comment ça marche, prenons l'exemple d'une multiplication suivie d'une addition, encore. La multiplication prend 8 cycles et occupe l'étage d'exécution pendant 8 cycles consécutifs. Pendant ces 8 cycles, l'instruction suivante attend dans l'unité de décodage, elle n'entre pas dans le chemin de donnée. Sa progression dans le pipeline est stoppée, et même que toutes les instructions précédentes. Elle ne redémarre que quand cela garantit que son écriture se fait après l'écriture de la multiplication. [[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]] Cette solution ''retarde les instructions'' pour qu'elles écrivent leurs résultats dans l'ordre. En clair, on retarde la seconde instruction histoire de remettre les écritures dans l'ordre. Lors de cette mise en attente, le pipeline est bloqué, mais seulement pour les étages précédant l'unité d'émission. Il s'agit donc d'un '''blocage sélectif''' du pipeline,n un blocage partiel. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Une implémentation basique bloque le pipeline quand une instruction multi-cycles est en cours d'exécution. Pour cela, l'unité d'émission contient un compteur, qu'il initialise quand il démarre une micro-opération multi-cycles. Le compteur est initialisé avec le nombre de cycles d'horloge passés dans l'unité de calcul. Une nouvelle micro-opération peut démarrer si ce compteur est à zéro, le pipeline est bloqué sinon. Cependant, les performances sont grandement en-deças de ce qu'on pourrait espérer. En effet, la micro-opération candidate est émise avec un retard largement plus grand que nécessaire. Par exemple, imaginons la situation illustrée dans le tableau ci-dessous. Une multiplication, suivie d'une addition qui écrase ce résultat au cycle suivant. Cas complètement irréaliste, mais passons. L'addition prend deux cycles, la multiplication en prend 5. Sans pipeline, voici ce que ça donne : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || Addition || Addition || class="f_bleu" | Écriture || |} Maintenant, ajoutons un pipeline, et utilisons l'unité d'émission décrite plus haut, qui retarde la seconde instruction tant que la première n'est pas terminée. Avec cette implémentation basique, l'addition est retardée tant que la multiplication n'a pas écrit son résultat dans le registre. Les cycles de retard sont indiqués en rouge. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Alors qu'en réalité, une solution optimale serait celle-ci : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Pour obtenir le cas optimal, le processeur utilise un '''''Result Shift Register'''''. Il s'agit d'un registre à décalage, dont le nombre de bits se cale sur l'instruction la plus longue. Par exemple, si le processeur exécute des additions en deux cycles, des décalages en trois cycles et des multiplications en 6 cycles, alors le ''result shift register'' aura 6 bits. Fait très important : on suppose que l'étage de ''writeback'' est comptée dans ces cycles. Et il en est de même pour les étages situés après ou avant l'étage d'exécution, du moment qu'ils suivent l'étage d'émission/le décodeur. Quand une instruction est émise, elle réserve le cycle d'horloge où elle compte faire une écriture. Les autres instructions ne pourront pas faire d'écriture lors d'un cycle réservé. Cela résout le problème des ''double écriture simultanées''. Pour réserver un cycle d'horloge, elle met le bit correspondant à 1, dans le ''result shift register''. Et on peut étendre la méthode pour gérer les dépendances WAW. Une instruction future ne doit pas faire d'écriture avant la micro-opération candidate. Pour cela, elle va empêcher les futures instructions de réserver les cycles précédents, ceux avant le cycle réservé. En clair : elle met les bits correspondant à 1. S'ils le sont déjà, ce n'est pas un problème : ils étaient à 1, ils restent à 1. les réservations faites par les instructions passées ne sont pas écrasées. Ainsi, une instruction qui s'exécute en N cycles réservera N étages de pipeline, N cycles consécutifs. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || 1 || 1 || 1 || 1 |} À chaque cycle d'horloge, ce registre est décalé d'un cran vers la droite. Cela permet de prendre en compte le fait que chaque instruction progresse d'un étage à chaque cycle d'horloge. Il va de soi que ce décalage est un décalage logique, qui remplit les vides par des zéros. Lorsque l'unité d'émission reçoit une instruction, elle vérifie le nombre de cycles qu'elle prend pour s’exécuter. Le nombre de cycles en question permet de savoir quel bit consulter dans le ''result shift''. Pour une instruction qui prend N cycles, elle vérifie si le bit numéro N de ce registre (en partant de la droite). S’il est à 1, cela signifie qu'une autre instruction a déjà réservé cet étage de pipeline. L'instruction candidate va devoir attendre que cet étage soit libre. Si ce bit est à 0, l’unité réserve alors le cycle associé en mettant le bit à 1, ainsi que tous les bits précédents, puis elle est émise. Par exemple, si je prends le cas du ''result shift register'' du tableau précédent. Imaginons qu'une instruction veuille s’exécuter pendant 4 cycles. L'unité d'émission regarde le quatrième bit. Elle voit qu'il est à 1, ce qui fait que ce n'est pas possible. Elle est alors retardée de un cycle. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 1 || 1 || 1 || 1 |} Après le retard, le ''result shift register'' a été décalé, ce qui donne : {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 0 || 1 || 1 || 1 |} Le bit associé vaut 0, ce qui fait que l'instruction peut être émise. Elle met alors ce bit à 1, ainsi que tous les précédents (ils le sont déjà). ===La solution des processeurs MIPS=== Une solution alternative a été utilisée sur les premiers processeurs MIPS. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications. Les multiplications étaient les seules instructions multi-cycles sur ces processeurs. Les instructions multi-cycles disposaient de leur propre registres rien que pour elles. Précisément, les instructions de multiplication enregistraient leur résultat dans des registres LO et HI, pas dans les registres généraux. Le fait de séparer les registres résout ce problème d'inversion des écritures. L'inversion des écritures survient quand on mixe des instructions avec des temps d'exécution différents, mais qui écrivent dans les mêmes registres. Ici, on sépare les registres et les instructions suivant leurs temps d'exécution. Les multiplications étaient émises dans l'ordre, avaient toute le même temps d'exécution : les résultats arrivaient forcément en ordre dans les registres HI/LO. Idem pour les autres instructions qui prenaient un seul cycle : elles étaient émises dans l'ordre, prenaient toutes un cycle, et enregistraient leurs résultats dans l'ordre dans les registres généraux. ==Les dépendances d'instruction== Les problèmes liés aux instructions multi-cycles sont des cas particuliers d'un problème plus général, que tous les pipelines ont. Un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée. Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, les instructions ont des dépendances très variées. Les dépendances qui vont nous intéresser sont les '''dépendances de données''' et les '''dépendances structurelles'''. Il y a aussi les '''dépendances de contrôle''' sont le fait des branchements, interruptions et exceptions. Elles ont fait l'objet des chapitres précédents. ===Les dépendances de données : registres et adresses=== Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c’est-à-dire au même registre ou la même adresse mémoire. Et cela nous amène à distinguer deux types de dépendances : de registre, d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité n'est pas réservée aux pipelines complexes. Nous verrons d'ailleurs un exemple frappant dans la fin de ce chapitre. Mais il faut avouer que les dépendances d'adresse ne posent problème que sur les processeurs dotés d'exécution dans le désordre, qui seront vus dans plusieurs chapitres. Aussi, pour le moment, concentrons-nous sur les dépendances de registres. Les dépendances de registre surviennent quand deux instructions lisent ou écrivent un registre. Deux instructions, deux types d'accès (lecture ou écriture), cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent le même registre, mais pas en même temps. * '''Read after write''' : Une instruction lit le résultat écrit par l'autre. * '''Write after write''' : Deux instructions écrivent dans le même registre, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances sont bloquantes. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Concrètement, elles n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW existent à condition que le pipeline supporte les écritures dans le désordre. Or, nous avons vu plus haut comment remettre les écritures dans l'ordre. Les pipelines que nous avons abordés ne sont pas concernés par les dépendances WAW. Aussi, nous repoussons leur étude aux processeurs avec exécution dans le désordre. ===L'unité d’émission : la détection des dépendances de registres=== Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions, ce qui peut être fait après l'unité de décodage. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites, qui sont souvent calculées/déterminées à l'exécution. Les deux types de dépendances sont donc le fait de circuits séparés, dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire, les autres dépendances sont gérées dans une '''unité d'émission''', séparée du décodeur d'instruction, qui a son propre étage de pipeline. L'unité d'émission détecte les dépendances de registres et met en attente les micro-opérations si besoin. Pour simplifier, c'est elle bloque le pipeline quand c'est nécessaire. Et mine de rien, la section sur les instructions multi-cycle nous a montré son rôle. C'est elle qui gèle le pipeline lors d'une instruction multi-cycle. Par contre, elle ne gère pas les défauts de cache, qui sont gérés par l'unité mémoire. {|class="wikitable" |+ Pipeline basique, avec une unité d'émission |- ! Calcul du ''Program Counter'' !! Chargement !! Décodage !! class="f_rouge" | Émission !! Lecture des opérandes (registres) !! Exécution (ALU) !! ''Writeback'' |- | rowspan="2" | ''Program Counter'' | rowspan="2" | Cache d'instruction | rowspan="2" | Décodeur d'instruction | rowspan="2" class="f_rouge" | Unité d'émission | rowspan="2" | Port de lecture du banc de registre (registres) | Unités de calcul | rowspan="2" | Port d'écriture du banc de registre |- | class="f_rouge" | Unité mémoire |} À chaque cycle, l'unité d'émission reçoit une micro-opération et analyse ses dépendances de données. Si elle a une dépendance bloquante, elle est mise en attente dans l'étage d'émission, en attendant que les conditions soient réunies pour pouvoir s'exécuter. Durant ce temps d'attente, le pipeline est bloqué. Sinon, l'unité d'émission la laisse passer à l'étage suivant. Commençons par étudier les dépendances RAW, où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction qui produit le résultat est appelée l''''instruction productrice''', alors que celle qui l'utilise est appelée l''''instruction consommatrice'''. Les deux instructions ont alors une dépendance de donnée, dite de type '''''Read After Write'''''. Et cette dépendance impose que l'instruction productrice soit totalement terminée avant qu'on émette l'instruction consommatrice. La raison est que le résultat de l'instruction productrice doit être enregistré dans les registres, avant d'être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Du fait de ce délai, il se peut que l'instruction consommatrice lise le registre avant que le résultat soit enregistré. Si l'instruction consommatrice est lancée immédiatement après l'instruction productrice, ça arrivera avec certitude. Pour éviter cela, il suffit de retarder l'exécution de l'instruction consommatrice tant que le résultat de l'instruction productrice n'est pas disponible, pas encore dans les registres. Et il se peut qu'elle soit retardée durant quelques cycles pour cela. Et ce retard implique, vous l'aurez deviné, de bloquer le pipeline. Précision importante : les dépendances RAW ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. Mais comment le processeur détecte les dépendances de registres entre deux instructions ? La réponse est simple : il compare les registres utilisés par les deux instructions. Les registres étant connus dès la sortie de l'étape de décodage, l'unité de décodage peut les comparer. L'unité d'émission reçoit une micro-opération décodée, appelée '''micro-opération candidate'''. Elle détecte si cette micro-opération candidate a une dépendance avec les micro-opérations qui sont dans le pipeline, dans les étages suivant l'unité d'émission. Les micro-opérations en question seront appelées '''micro-opérations "en vol"'''. L'unité d'émission compare les registres utilisés par la micro-opération candidate et les micro-opérations en vol. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Une dépendance RAW a lieu entre une instruction consommatrice et une instruction productrice. L'instruction productrice écrit son résultat dans un registre, son registre de destination. L'instruction consommatrice utilise ce résultat comme opérande, ce qui fait que le résultat est dans un de ses registres opérande. Une dépendance RAW implique que l'instruction productrice est en vol, alors que l'instruction consommatrice est candidate. L'unité d'émission doit donc comparer les registres opérandes de l'instruction candidate, avec les registres de destination des instructions en vol. L'unité d'émission doit donc connaitre les registres de destination des micro-opérations en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons. [[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]] Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être lu par une autre instruction. Les registres qui ne sont pas réservés sont dits libres ou 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é, ce qui implique que c'est le registre de destination d'une instruction en vol. L'instruction n'est pas émise et le pipeline est bloqué. Le registre encode les registres réservés en représentation ''one-hot'' modifiée. 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. Un registre est réservé en mettant le bit associé à 1. {|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 |} Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''. 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 l'instruction, 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 l'instruction 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. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. 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 l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul. Diverses optimisations permettent de mitiger l'impact des dépendances RAW. Et dans la section suivante, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. ==Le contournement : réduire les dépendances== Nous venons de voir que les dépendances imposent de bloquer le pipeline, pendant quelques cycles. Et encore une fois, c'est l'unité d'émission qui s'en charge. Du moins, c'est le cas sans optimisation. Une optimisation permet de fortement réduire les blocages de pipelines liés aux dépendances RAW, jusqu'à les annuler dans certains cas. L'optimisation en question est celle du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] Le contournement fait que deux instructions consécutives avec une dépendance RAW peuvent quand même être émises. Mais comme on va le voir, cela n'élimine pas les blocages de pipelines liés aux dépendances RAW. L'unité d'émission existe toujours, elle n'est pas forcément simplifiée, au contraire. Par contre, les blocages de pipeline sont moins fréquents, et les performances augmentent. : Avant de poursuivre, rappelons le vocabulaire. Si une instruction produit un résultat, qui utilisé par une seconde instruction comme opérande : la première instruction est appelée l''''instruction productrice''', la seconde est appelée l''''instruction consommatrice'''. ===Le contournement de l'unité de calcul=== La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée. [[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]] Le résultat d'une opération est mémorisé en sortie de l'ALU, et précisément dans le registre de pipeline, au cycle suivant. Le contournement connecte ce registre de pipeline sur l'entrée de l'ALU si besoin, et on le déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs. [[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]] Intuitivement, on se dit que c'est l'unité d'émission qui commande le multiplexeur. Mais il est possible de déporter cette fonction en-dehors de l'unité d'émission, de gérer cela au niveau "local". La commande du multiplexeur est réalisée par un circuit situé pas loin de l'ALU elle-même. L'unité d'émission émet juste deux micro-opérations consécutives qui ont une dépendance RAW, à condition que le résultat de l'une soit immédiatement consommé par l'autre, et que les deux utilisent l'ALU entière. Pour commander les multiplexeurs, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul. [[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]] La présence de plusieurs unités de calcul complexifie le contournement. Un processeur a typiquement une ALU entière, un ''barrel shifter'' et un multiplieur entier. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque circuit à la fois sur son entrée, mais aussi sur l'entrée des deux autres. En tout, cela demande 9 interconnexions, avec autant de multiplexeurs. Et c'est la même chose à l'intérieur d'une FPU. Pour rappel une FPU est typiquement composée de deux circuits : un additionneur flottant et un multiplieur flottant. Non seulement ils ont leur sortie connectée à leur entrée, mais en plus, la sortie de l'un est relié à l'entrée de l'autre. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand-chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies entre registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un entre FPUs. [[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]] Il faut aussi penser au cas où les ALU entières calculent des adresses mémoire. Dans ce cas, il faut aussi connecter une unité de calcul à l'unité mémoire. Le ''barrel shifter'' et le multiplieur ne sont pas concernés, seule l'ALU entière capable de faire des additions l'est. La connexion est unidirectionnelle, de l'ALU entière vers l'unité mémoire. Si l'unité mémoire incorpore sa propre unité de calcul, il faut faire de même, mais pas pour les mêmes raisons. Le réseau de contournement ne transmet alors pas des adresses mémoire calculées, mais les opérandes de ces calculs. Ces opérandes viennent soit des registres, soit sont calculées dans l'ALU entière. Mais le réseau de contournement est alors plus complexe, car toutes les ALU entières peuvent calculer ces opérandes. En pratique, c'est rarement fait. L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires. ===Le contournement des lectures mémoire=== La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand une lecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué). Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre. Et cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire. [[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]] La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs. [[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]] Ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt. [[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]] La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux. [[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]] La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour. ===Le contournement des instructions multi-cycles=== La durée des instructions est à prendre en compte pour effectuer un contournement convenable. Le contournement entre instructions simples est facile, vu qu'elles s'exécutent en un seul cycle. Leur résultat peut être envoyé immédiatement sur leur entrée. Il n'y a aucun besoin de bloquer le pipeline. Mais avec les instructions multi-cycles, les choses sont plus compliquées, et cela mérite un exemple explicatif. Un exemple est celui où une multiplication de 5 cycles est suivie par une addition de deux cycles. Si les deux instructions sont indépendantes, la seconde sera retardée, en raison des dépendances WAW, mais pas à cause des dépendances RAW. Voici comment sont exécutées les deux instructions : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Maintenant, supposons que l'addition a pour opérande le résultat de la multiplication. On voit que l'on ne peut pas reprendre tel quel le tableau précédent, car l'addition serait émise trop tôt. Il faut donc retarder encore l'addition, pour qu'elle démarre quand la multiplication vient tout juste de terminer. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} L'unité d'émission retarde l'émission d'une instruction pendant quelques cycles, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas. Le contournement doit aussi tenir compte des registres d'échelonnage. Il arrive en effet qu'une instruction soit émise alors que son opérande est encore dans les registres d'échelonnage. Ce n'est pas rare, c'est même assez fréquent. Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Mais lire les opérandes dans les registres d'échelonnage nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin. ===L’émission anticipée des micro-opérations=== Idéalement, on voudrait démarrer une nouvelle micro-opération sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU. Et cela peut poser problème pour le contournement. Imaginons que l'on ait trois cycles entre l'unité d'émission et les ALU. Si une instruction est émise quand son opérande sort de l'ALU, elle prend plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''. Le nombre de cycles d'avance est égal au nombre d'étages entre l'unité de calcul et l'unité d'émission. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache. Et il peut se le permettre car le pipeline est bloqué en cas de défaut de cache. Un défaut de cache gèlera le pipeline, ce qui fait que l'instruction arrivera en entrée de l'ALU au bon moment, quand le pipeline sera dégelé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'émission des instructions | prevText=L'émission des instructions | next=Le scoreboard et le ROB | nextText=Le scoreboard et le ROB }} </noinclude> 7y0j8fnjopxj0w0bvajtbfv3i1fznee 775075 775059 2026-10-12T18:48:42Z Mewtow 31375 /* Les dépendances d'instruction */ 775075 wikitext text/x-wiki Avec ce qu'on a dit dans le chapitre précédent, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipeline doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on va expliquer dans ce chapitre. Dans ce chapitre, nous allons voir les '''processeurs ''in-order'''''. Les définir n'est pas facile, surtout à ce stade du cours. Ils sont opposés aux processeurs avec exécution dans le désordre, qu'on verra dans quelques chapitres. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. On démarre les instructions une par une, dans un certain ordre. Mais le parcours du pipeline n'est pas un long fleuve tranquille, comme on le verra dans ce chapitre, et le résultat à la fin du pipeline est parfois différent. À la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Au final, deux instructions peuvent théoriquement arriver dans le désordre. Du moins, ce serait le cas si le processeur n'était pas conçu pour corriger le problème. Les processeurs ''in-order'' garantissent que des instructions chargées dans un certain ordre termineront dans le même ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner au mieux. Les ''processeurs à exécution dans le désordre'', eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance. Mais ils ont des systèmes pour éviter que cela pose problème. Ils utilisent pour cela des systèmes d'exception précises plus élaborés que ceux des chapitres précédents. Mais cela sera pour un autre chapitre. Pour ce chapitre, nous allons nous concentrer sur les processeurs ''in-order''. Mais avant toute chose, un petit rappel. Un processeur avec un pipeline est décomposé en deux parties. Le ''front end''), prend en charge les étages communs à toutes les instructions, comme le chargement et le décodage. L'amont est suivi une unité d'émission, elle-même suivie par le chemin de données. Le chemin de données contient plusieurs unités différentes : des unités de calcul, l'unité mémoire, parfois des unités pour les branchements. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] Avec des instructions multi-cycles, le pipeline exploite plusieurs unités en même temps, c'est même la norme. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. Et le reste du chapitre vise à expliquer quelles sont ces contraintes. ==Les défauts de cache bloquent le pipeline== Une instruction mémoire a un temps d'exécution variable, en raison des défauts de cache. En général, elle fait un succès de cache et accède au cache L1 de données. L'accès mémoire se fait en un cycle, parfois 2 cycles si le cache est pipeliné. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : une dizaine de cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation. Et si le défaut de cache se double d'un défaut de page mémoire, l'instruction peut dépasser le millier de cycles d'horloge. Les pipelines précédents doivent donc gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Un problème est que les accès mémoire ne peuvent pas être gérés comme les autres instructions multi-cycles, car ce sont les seules instructions au temps d'exécution variable, et imprédictible. La solution utilisée par tous les processeurs dits ''in-order'', suppose que les instructions mémoire font un succès de cache et s'exécutent en un cycle d'horloge. Si un défaut de cache a lieu, on bloque le pipeline. ===Le blocage du pipeline par l'unité mémoire=== Pour geler le pipeline, l'unité mémoire génère un signal ''pipeline stall''. Il est mis à 1 pendant qu'un défaut de cache est en cours, il est remis à zéro une fois le défaut de cache terminé. Ce signal est utilisé pour geler les registres du pipeline, dans les étages précédents. Seuls les étages situés avant l'étage MEM sont gelés. Ceux situés après peuvent faire leur travail normalement, ce qui permet au système d'exceptions précises de faire son travail. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé. [[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]] La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de cache est en cours. C'est seulement quand le défaut est terminé et qu'il reçoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline. ===Pourquoi bloquer le pipeline en cas de défaut de cache ?=== Mais pourquoi bloquer le pipeline en cas de défaut de cache ? Par exemple, si une lecture déclenche un défaut de cache, pourquoi ne pas continuer à émettre les instructions après la lecture ? En théorie, c'est possible, c'est même ce que font les processeurs à exécution dans le désordre. Ils ne sont pas bloqués après un défaut de cache. Cependant, ils peuvent le faire car ils ont un système d'exception précise élaboré, pas les processeurs ''in-order''. Si les instructions après la lecture continuaient, elles finirait par atteindre la fin du pipeline, voire par le quitter. Un défaut de cache dure longtemps, une dizaine de cycles, voire plus. C'est souvent plus de cycles que la totalité du pipeline du processeur. Et encore plus que le nombre de cycles avant l'unité mémoire. Le résultat est que ces instructions quitteraient le pipeline, écriraient dans les registres, voire dans la mémoire RAM, et ce serait définitif. Si la lecture déclenche une exception matérielle, on ne pourrait pas revenir en arrière, le système d'exception précise serait en erreur. Néanmoins, cela suggère une optimisation. L'idée est de continuer à exécuter des instructions après le défaut de cache. L'unité mémoire est bloquée, elle ne peut pas accepter de nouvelle instruction, mais l'ALU entière, la FPU et les autres circuits sont utilisables. Les instructions émises après le défaut de cache sont alors appelées les ''instructions en avance''. Elles n'ont pas le droit d'enregistrer leurs résultats, que ce soit dans les registres ou la RAM. Pour cela, les micro-opérations restent dans le pipeline et sont stoppées dans le dernier étage libre. Par exemple, imaginons qu'un défaut de cache ait lieu à l'instant T, au cycle T. La lecture va se propager dans le pipeline jusqu'à l'étage de ''writeback'', dont à la toute fin du pipeline. La micro-opération suivante, émise au cycle T+2, s’arrêtera à l'étage d'avant. La micro-opération encore avant s’arrêtera à l'étage encore avant, et ainsi de suite. Le pipeline finira par se remplir totalement, et l'unité d'émission n'aura pas le choix que de bloquer le pipeline. Le résultat est que des micro-opérations auront été préchargées et exécutées à l'avance, mais ne se seront pas "terminées". Il y a toujours la possibilité de les annuler, en cas de problèmes. Le nombre de micro-opérations chargées en avance est égal au nombre d'étage du pipeline, vu qu'il est totalement remplit. Cette optimisation a possiblement été utilisée par les processeurs ARM de marque Cortex A53, A55 et A510, bien que ce ne soit pas certain (il s'agit d'une supposition fite par l'auteur du bloc "Chips and cheese". Le Cortex A52 et le A55 pouvaient exécuter en avance 8 instructions, et ils avaient un pipeline de 8 étages, alors que le A510 pouvait avoir 12 instructions en avance, et il avait justement un pipeline de 12 étages. {|class="wikitable" |- ! Fetch 1 !! Fetch 2 !! Décodage 1 !! Décodage 2 !! Émission !! Lecture des registres !! Exécution !! ''Writeback'' |- | Micro-opération T + 8 || Micro-opération T + 7 || Micro-opération T + 6 || Micro-opération T + 5 || Micro-opération T + 4 || Micro-opération T + 3 || Micro-opération T + 2 || Micro-opération T + 1 || colspan="8" class="f_rouge" | Lecture, en défaut au cycle T |} ==L'inversion de l'ordre des écritures== Démarrer une nouvelle instruction par cycle est la base du pipeline. Mais avec des instructions multi-cycles, un problème bien particulier survient. Le processeur exécute plusieurs instructions en même temps, dans le même étage d’exécution, mais dans des ALUS/multiplieurs/FPU séparés. Le problème, c'est que si les instructions démarrent dans l'ordre du programme, les unes après les autres, elles peuvent terminer dans le désordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé ! Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre. On dit qu'il y a '''inversion de l'ordre des écritures'''. Et cette inversion cause de nombreux problèmes. Premier problème : des registres peuvent avoir une valeur invalide. Si les écritures changent d'ordre, une instruction peut lire un opérande qui n'est pas la bonne, qui n'a pas été écrite par la bonne instruction. Le programme ne s'exécute pas correctement. De plus, les mécanismes d'exception précises cessent de fonctionner, car ils supposent que les écritures dans les registres se font dans l'ordre. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Maintenant, autre problème, qui peut être vu comme un cas particulier du précédent. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Il y a un phénomène de '''double écriture simultanées'''. Et s'il s'agit là d'un exemple qui mélange dépendance structurelle et dépendance WAW. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! ... |- | Multiplication || Multiplication || Multiplication ||class="f_bleu" | Écriture || || |- | || Addition || Addition || class="f_bleu" | Ecriture || |} Il y a deux solutions principales pour régler ce problème, avec une troisième solution spécialement adaptée aux instructions mémoire, qui sont à part des autres. La première méthode permet d'utiliser des instructions multi-cycles, avec un pipeline fixe. La seconde utilise soit un pipeline fixe, soit un pipeline dynamique, mais avec des restrictions assez importantes. La gestion des défauts de cache est elle un cas à part, valable sur tous les types de pipelines. ===Les registres d'échelonnement=== La première solution est assez brute de décoffrage. Elle force toutes les instructions à prendre exactement le même nombre de cycles d'horloge. Elle utilise donc un pipeline où toutes les instructions ont le même nombre d'étages. Un tel pipeline est appelé un ''pipeline de longueur fixe'', ou encore un '''pipeline fixe'''. Et le processeur se cale sur les instructions les plus lentes. Par exemple, si une addition prend un cycle d'horloge, qu'une multiplication en prend 8 et qu'un décalage en prend 2, alors toutes les instructions prendront 8 cycles d'horloge. Et ces 8 cycles d'horloge correspondront à 8 étages d'exécution. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] Un défaut de la méthode précédente est que les données sont copiées d'un registre de pipeline au suivant à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs ''in order'' modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé. [[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]] ===Les blocages de pipeline=== Une solution alternative n'utilise ni registres d'échelonnage, ni étages qui ne font rien. Elle utilise à la place un '''pipeline dynamique''', de taille variable selon l'instruction. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. Les instructions multi-cycles prennent le temps qu'il faut pour faire leur calcul, les additions donnent leur résultat en un seul cycle, bref : les instructions ont des temps d'exécution variables. On ne se cale pas sur la plus lente. De plus, les instructions écrivent dans les registres au plus tôt, dès qu'elles ont fini leur calcul. [[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]] Pour éviter d'inverser les écritures, ce sont les instructions qui sont retardées. Pour comprendre comment ça marche, prenons l'exemple d'une multiplication suivie d'une addition, encore. La multiplication prend 8 cycles et occupe l'étage d'exécution pendant 8 cycles consécutifs. Pendant ces 8 cycles, l'instruction suivante attend dans l'unité de décodage, elle n'entre pas dans le chemin de donnée. Sa progression dans le pipeline est stoppée, et même que toutes les instructions précédentes. Elle ne redémarre que quand cela garantit que son écriture se fait après l'écriture de la multiplication. [[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]] Cette solution ''retarde les instructions'' pour qu'elles écrivent leurs résultats dans l'ordre. En clair, on retarde la seconde instruction histoire de remettre les écritures dans l'ordre. Lors de cette mise en attente, le pipeline est bloqué, mais seulement pour les étages précédant l'unité d'émission. Il s'agit donc d'un '''blocage sélectif''' du pipeline,n un blocage partiel. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Une implémentation basique bloque le pipeline quand une instruction multi-cycles est en cours d'exécution. Pour cela, l'unité d'émission contient un compteur, qu'il initialise quand il démarre une micro-opération multi-cycles. Le compteur est initialisé avec le nombre de cycles d'horloge passés dans l'unité de calcul. Une nouvelle micro-opération peut démarrer si ce compteur est à zéro, le pipeline est bloqué sinon. Cependant, les performances sont grandement en-deças de ce qu'on pourrait espérer. En effet, la micro-opération candidate est émise avec un retard largement plus grand que nécessaire. Par exemple, imaginons la situation illustrée dans le tableau ci-dessous. Une multiplication, suivie d'une addition qui écrase ce résultat au cycle suivant. Cas complètement irréaliste, mais passons. L'addition prend deux cycles, la multiplication en prend 5. Sans pipeline, voici ce que ça donne : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || Addition || Addition || class="f_bleu" | Écriture || |} Maintenant, ajoutons un pipeline, et utilisons l'unité d'émission décrite plus haut, qui retarde la seconde instruction tant que la première n'est pas terminée. Avec cette implémentation basique, l'addition est retardée tant que la multiplication n'a pas écrit son résultat dans le registre. Les cycles de retard sont indiqués en rouge. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Alors qu'en réalité, une solution optimale serait celle-ci : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Pour obtenir le cas optimal, le processeur utilise un '''''Result Shift Register'''''. Il s'agit d'un registre à décalage, dont le nombre de bits se cale sur l'instruction la plus longue. Par exemple, si le processeur exécute des additions en deux cycles, des décalages en trois cycles et des multiplications en 6 cycles, alors le ''result shift register'' aura 6 bits. Fait très important : on suppose que l'étage de ''writeback'' est comptée dans ces cycles. Et il en est de même pour les étages situés après ou avant l'étage d'exécution, du moment qu'ils suivent l'étage d'émission/le décodeur. Quand une instruction est émise, elle réserve le cycle d'horloge où elle compte faire une écriture. Les autres instructions ne pourront pas faire d'écriture lors d'un cycle réservé. Cela résout le problème des ''double écriture simultanées''. Pour réserver un cycle d'horloge, elle met le bit correspondant à 1, dans le ''result shift register''. Et on peut étendre la méthode pour gérer les dépendances WAW. Une instruction future ne doit pas faire d'écriture avant la micro-opération candidate. Pour cela, elle va empêcher les futures instructions de réserver les cycles précédents, ceux avant le cycle réservé. En clair : elle met les bits correspondant à 1. S'ils le sont déjà, ce n'est pas un problème : ils étaient à 1, ils restent à 1. les réservations faites par les instructions passées ne sont pas écrasées. Ainsi, une instruction qui s'exécute en N cycles réservera N étages de pipeline, N cycles consécutifs. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || 1 || 1 || 1 || 1 |} À chaque cycle d'horloge, ce registre est décalé d'un cran vers la droite. Cela permet de prendre en compte le fait que chaque instruction progresse d'un étage à chaque cycle d'horloge. Il va de soi que ce décalage est un décalage logique, qui remplit les vides par des zéros. Lorsque l'unité d'émission reçoit une instruction, elle vérifie le nombre de cycles qu'elle prend pour s’exécuter. Le nombre de cycles en question permet de savoir quel bit consulter dans le ''result shift''. Pour une instruction qui prend N cycles, elle vérifie si le bit numéro N de ce registre (en partant de la droite). S’il est à 1, cela signifie qu'une autre instruction a déjà réservé cet étage de pipeline. L'instruction candidate va devoir attendre que cet étage soit libre. Si ce bit est à 0, l’unité réserve alors le cycle associé en mettant le bit à 1, ainsi que tous les bits précédents, puis elle est émise. Par exemple, si je prends le cas du ''result shift register'' du tableau précédent. Imaginons qu'une instruction veuille s’exécuter pendant 4 cycles. L'unité d'émission regarde le quatrième bit. Elle voit qu'il est à 1, ce qui fait que ce n'est pas possible. Elle est alors retardée de un cycle. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 1 || 1 || 1 || 1 |} Après le retard, le ''result shift register'' a été décalé, ce qui donne : {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 0 || 1 || 1 || 1 |} Le bit associé vaut 0, ce qui fait que l'instruction peut être émise. Elle met alors ce bit à 1, ainsi que tous les précédents (ils le sont déjà). ===La solution des processeurs MIPS=== Une solution alternative a été utilisée sur les premiers processeurs MIPS. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications. Les multiplications étaient les seules instructions multi-cycles sur ces processeurs. Les instructions multi-cycles disposaient de leur propre registres rien que pour elles. Précisément, les instructions de multiplication enregistraient leur résultat dans des registres LO et HI, pas dans les registres généraux. Le fait de séparer les registres résout ce problème d'inversion des écritures. L'inversion des écritures survient quand on mixe des instructions avec des temps d'exécution différents, mais qui écrivent dans les mêmes registres. Ici, on sépare les registres et les instructions suivant leurs temps d'exécution. Les multiplications étaient émises dans l'ordre, avaient toute le même temps d'exécution : les résultats arrivaient forcément en ordre dans les registres HI/LO. Idem pour les autres instructions qui prenaient un seul cycle : elles étaient émises dans l'ordre, prenaient toutes un cycle, et enregistraient leurs résultats dans l'ordre dans les registres généraux. ==Les dépendances d'instruction== Les problèmes liés aux instructions multi-cycles sont des cas particuliers d'un problème plus général, que tous les pipelines ont. Un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée. Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, les instructions ont des dépendances très variées. Les dépendances qui vont nous intéresser sont les '''dépendances de données'''. Nous avons déjà parlé des '''dépendances structurelles''' au chapitre précédent, aussi je ne reviendrais pas dessus. Il y a aussi les '''dépendances de contrôle''' sont le fait des branchements, interruptions et exceptions, qui fait l'objet des chapitres sur la prédiction de branchement, et d'une section sur les exceptions précises. ===Les dépendances de données : registres et adresses=== Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c’est-à-dire au même registre ou la même adresse mémoire. Et cela nous amène à distinguer deux types de dépendances : de registre, d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité n'est pas réservée aux pipelines complexes. Nous verrons d'ailleurs un exemple frappant dans la fin de ce chapitre. Mais il faut avouer que les dépendances d'adresse ne posent problème que sur les processeurs dotés d'exécution dans le désordre, qui seront vus dans plusieurs chapitres. Aussi, pour le moment, concentrons-nous sur les dépendances de registres. Les dépendances de registre surviennent quand deux instructions lisent ou écrivent un registre. Deux instructions, deux types d'accès (lecture ou écriture), cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent le même registre, mais pas en même temps. * '''Read after write''' : Une instruction lit le résultat écrit par l'autre. * '''Write after write''' : Deux instructions écrivent dans le même registre, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances sont bloquantes. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Concrètement, elles n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW existent à condition que le pipeline supporte les écritures dans le désordre. Or, nous avons vu plus haut comment remettre les écritures dans l'ordre. Les pipelines que nous avons abordés ne sont pas concernés par les dépendances WAW. Aussi, nous repoussons leur étude aux processeurs avec exécution dans le désordre. ===L'unité d’émission : la détection des dépendances de registres=== Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions, ce qui peut être fait après l'unité de décodage. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites, qui sont souvent calculées/déterminées à l'exécution. Les deux types de dépendances sont donc le fait de circuits séparés, dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire, les autres dépendances sont gérées dans une '''unité d'émission''', séparée du décodeur d'instruction, qui a son propre étage de pipeline. L'unité d'émission détecte les dépendances de registres et met en attente les micro-opérations si besoin. Pour simplifier, c'est elle bloque le pipeline quand c'est nécessaire. Et mine de rien, la section sur les instructions multi-cycle nous a montré son rôle. C'est elle qui gèle le pipeline lors d'une instruction multi-cycle. Par contre, elle ne gère pas les défauts de cache, qui sont gérés par l'unité mémoire. {|class="wikitable" |+ Pipeline basique, avec une unité d'émission |- ! Calcul du ''Program Counter'' !! Chargement !! Décodage !! class="f_rouge" | Émission !! Lecture des opérandes (registres) !! Exécution (ALU) !! ''Writeback'' |- | rowspan="2" | ''Program Counter'' | rowspan="2" | Cache d'instruction | rowspan="2" | Décodeur d'instruction | rowspan="2" class="f_rouge" | Unité d'émission | rowspan="2" | Port de lecture du banc de registre (registres) | Unités de calcul | rowspan="2" | Port d'écriture du banc de registre |- | class="f_rouge" | Unité mémoire |} À chaque cycle, l'unité d'émission reçoit une micro-opération et analyse ses dépendances de données. Si elle a une dépendance bloquante, elle est mise en attente dans l'étage d'émission, en attendant que les conditions soient réunies pour pouvoir s'exécuter. Durant ce temps d'attente, le pipeline est bloqué. Sinon, l'unité d'émission la laisse passer à l'étage suivant. Commençons par étudier les dépendances RAW, où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction qui produit le résultat est appelée l''''instruction productrice''', alors que celle qui l'utilise est appelée l''''instruction consommatrice'''. Les deux instructions ont alors une dépendance de donnée, dite de type '''''Read After Write'''''. Et cette dépendance impose que l'instruction productrice soit totalement terminée avant qu'on émette l'instruction consommatrice. La raison est que le résultat de l'instruction productrice doit être enregistré dans les registres, avant d'être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Du fait de ce délai, il se peut que l'instruction consommatrice lise le registre avant que le résultat soit enregistré. Si l'instruction consommatrice est lancée immédiatement après l'instruction productrice, ça arrivera avec certitude. Pour éviter cela, il suffit de retarder l'exécution de l'instruction consommatrice tant que le résultat de l'instruction productrice n'est pas disponible, pas encore dans les registres. Et il se peut qu'elle soit retardée durant quelques cycles pour cela. Et ce retard implique, vous l'aurez deviné, de bloquer le pipeline. Précision importante : les dépendances RAW ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. Mais comment le processeur détecte les dépendances de registres entre deux instructions ? La réponse est simple : il compare les registres utilisés par les deux instructions. Les registres étant connus dès la sortie de l'étape de décodage, l'unité de décodage peut les comparer. L'unité d'émission reçoit une micro-opération décodée, appelée '''micro-opération candidate'''. Elle détecte si cette micro-opération candidate a une dépendance avec les micro-opérations qui sont dans le pipeline, dans les étages suivant l'unité d'émission. Les micro-opérations en question seront appelées '''micro-opérations "en vol"'''. L'unité d'émission compare les registres utilisés par la micro-opération candidate et les micro-opérations en vol. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Une dépendance RAW a lieu entre une instruction consommatrice et une instruction productrice. L'instruction productrice écrit son résultat dans un registre, son registre de destination. L'instruction consommatrice utilise ce résultat comme opérande, ce qui fait que le résultat est dans un de ses registres opérande. Une dépendance RAW implique que l'instruction productrice est en vol, alors que l'instruction consommatrice est candidate. L'unité d'émission doit donc comparer les registres opérandes de l'instruction candidate, avec les registres de destination des instructions en vol. L'unité d'émission doit donc connaitre les registres de destination des micro-opérations en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons. [[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]] Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être lu par une autre instruction. Les registres qui ne sont pas réservés sont dits libres ou 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é, ce qui implique que c'est le registre de destination d'une instruction en vol. L'instruction n'est pas émise et le pipeline est bloqué. Le registre encode les registres réservés en représentation ''one-hot'' modifiée. 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. Un registre est réservé en mettant le bit associé à 1. {|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 |} Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''. 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 l'instruction, 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 l'instruction 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. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. 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 l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul. Diverses optimisations permettent de mitiger l'impact des dépendances RAW. Et dans la section suivante, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. ==Le contournement : réduire les dépendances== Nous venons de voir que les dépendances imposent de bloquer le pipeline, pendant quelques cycles. Et encore une fois, c'est l'unité d'émission qui s'en charge. Du moins, c'est le cas sans optimisation. Une optimisation permet de fortement réduire les blocages de pipelines liés aux dépendances RAW, jusqu'à les annuler dans certains cas. L'optimisation en question est celle du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] Le contournement fait que deux instructions consécutives avec une dépendance RAW peuvent quand même être émises. Mais comme on va le voir, cela n'élimine pas les blocages de pipelines liés aux dépendances RAW. L'unité d'émission existe toujours, elle n'est pas forcément simplifiée, au contraire. Par contre, les blocages de pipeline sont moins fréquents, et les performances augmentent. : Avant de poursuivre, rappelons le vocabulaire. Si une instruction produit un résultat, qui utilisé par une seconde instruction comme opérande : la première instruction est appelée l''''instruction productrice''', la seconde est appelée l''''instruction consommatrice'''. ===Le contournement de l'unité de calcul=== La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée. [[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]] Le résultat d'une opération est mémorisé en sortie de l'ALU, et précisément dans le registre de pipeline, au cycle suivant. Le contournement connecte ce registre de pipeline sur l'entrée de l'ALU si besoin, et on le déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs. [[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]] Intuitivement, on se dit que c'est l'unité d'émission qui commande le multiplexeur. Mais il est possible de déporter cette fonction en-dehors de l'unité d'émission, de gérer cela au niveau "local". La commande du multiplexeur est réalisée par un circuit situé pas loin de l'ALU elle-même. L'unité d'émission émet juste deux micro-opérations consécutives qui ont une dépendance RAW, à condition que le résultat de l'une soit immédiatement consommé par l'autre, et que les deux utilisent l'ALU entière. Pour commander les multiplexeurs, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul. [[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]] La présence de plusieurs unités de calcul complexifie le contournement. Un processeur a typiquement une ALU entière, un ''barrel shifter'' et un multiplieur entier. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque circuit à la fois sur son entrée, mais aussi sur l'entrée des deux autres. En tout, cela demande 9 interconnexions, avec autant de multiplexeurs. Et c'est la même chose à l'intérieur d'une FPU. Pour rappel une FPU est typiquement composée de deux circuits : un additionneur flottant et un multiplieur flottant. Non seulement ils ont leur sortie connectée à leur entrée, mais en plus, la sortie de l'un est relié à l'entrée de l'autre. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand-chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies entre registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un entre FPUs. [[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]] Il faut aussi penser au cas où les ALU entières calculent des adresses mémoire. Dans ce cas, il faut aussi connecter une unité de calcul à l'unité mémoire. Le ''barrel shifter'' et le multiplieur ne sont pas concernés, seule l'ALU entière capable de faire des additions l'est. La connexion est unidirectionnelle, de l'ALU entière vers l'unité mémoire. Si l'unité mémoire incorpore sa propre unité de calcul, il faut faire de même, mais pas pour les mêmes raisons. Le réseau de contournement ne transmet alors pas des adresses mémoire calculées, mais les opérandes de ces calculs. Ces opérandes viennent soit des registres, soit sont calculées dans l'ALU entière. Mais le réseau de contournement est alors plus complexe, car toutes les ALU entières peuvent calculer ces opérandes. En pratique, c'est rarement fait. L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires. ===Le contournement des lectures mémoire=== La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand une lecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué). Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre. Et cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire. [[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]] La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs. [[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]] Ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt. [[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]] La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux. [[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]] La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour. ===Le contournement des instructions multi-cycles=== La durée des instructions est à prendre en compte pour effectuer un contournement convenable. Le contournement entre instructions simples est facile, vu qu'elles s'exécutent en un seul cycle. Leur résultat peut être envoyé immédiatement sur leur entrée. Il n'y a aucun besoin de bloquer le pipeline. Mais avec les instructions multi-cycles, les choses sont plus compliquées, et cela mérite un exemple explicatif. Un exemple est celui où une multiplication de 5 cycles est suivie par une addition de deux cycles. Si les deux instructions sont indépendantes, la seconde sera retardée, en raison des dépendances WAW, mais pas à cause des dépendances RAW. Voici comment sont exécutées les deux instructions : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Maintenant, supposons que l'addition a pour opérande le résultat de la multiplication. On voit que l'on ne peut pas reprendre tel quel le tableau précédent, car l'addition serait émise trop tôt. Il faut donc retarder encore l'addition, pour qu'elle démarre quand la multiplication vient tout juste de terminer. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} L'unité d'émission retarde l'émission d'une instruction pendant quelques cycles, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas. Le contournement doit aussi tenir compte des registres d'échelonnage. Il arrive en effet qu'une instruction soit émise alors que son opérande est encore dans les registres d'échelonnage. Ce n'est pas rare, c'est même assez fréquent. Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Mais lire les opérandes dans les registres d'échelonnage nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin. ===L’émission anticipée des micro-opérations=== Idéalement, on voudrait démarrer une nouvelle micro-opération sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU. Et cela peut poser problème pour le contournement. Imaginons que l'on ait trois cycles entre l'unité d'émission et les ALU. Si une instruction est émise quand son opérande sort de l'ALU, elle prend plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''. Le nombre de cycles d'avance est égal au nombre d'étages entre l'unité de calcul et l'unité d'émission. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Le processeur émet des instructions en supposant qu'une lecture entrainera un succès de cache. Et il peut se le permettre car le pipeline est bloqué en cas de défaut de cache. Un défaut de cache gèlera le pipeline, ce qui fait que l'instruction arrivera en entrée de l'ALU au bon moment, quand le pipeline sera dégelé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'émission des instructions | prevText=L'émission des instructions | next=Le scoreboard et le ROB | nextText=Le scoreboard et le ROB }} </noinclude> k1oftwvyc7bu5sxsjfy46oxcvcwwf6r 775084 775075 2026-10-12T20:20:26Z Mewtow 31375 775084 wikitext text/x-wiki Avec ce qu'on a dit dans le chapitre précédent, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipeline doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on va expliquer dans ce chapitre. Dans ce chapitre, nous allons voir les '''processeurs ''in-order'''''. Les définir n'est pas facile, surtout à ce stade du cours. Ils sont opposés aux processeurs avec exécution dans le désordre, qu'on verra dans quelques chapitres. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. On démarre les instructions une par une, dans un certain ordre. Mais le parcours du pipeline n'est pas un long fleuve tranquille, comme on le verra dans ce chapitre, et le résultat à la fin du pipeline est parfois différent. À la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Au final, deux instructions peuvent théoriquement arriver dans le désordre. Du moins, ce serait le cas si le processeur n'était pas conçu pour corriger le problème. Les processeurs ''in-order'' garantissent que des instructions chargées dans un certain ordre termineront dans le même ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner au mieux. Les ''processeurs à exécution dans le désordre'', eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance. Mais ils ont des systèmes pour éviter que cela pose problème. Ils utilisent pour cela des systèmes d'exception précises plus élaborés que ceux des chapitres précédents. Mais cela sera pour un autre chapitre. Pour ce chapitre, nous allons nous concentrer sur les processeurs ''in-order''. Mais avant toute chose, un petit rappel. Un processeur avec un pipeline est décomposé en deux parties. Le ''front end''), prend en charge les étages communs à toutes les instructions, comme le chargement et le décodage. L'amont est suivi une unité d'émission, elle-même suivie par le chemin de données. Le chemin de données contient plusieurs unités différentes : des unités de calcul, l'unité mémoire, parfois des unités pour les branchements. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] Avec des instructions multi-cycles, le pipeline exploite plusieurs unités en même temps, c'est même la norme. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. Et le reste du chapitre vise à expliquer quelles sont ces contraintes. ==L'inversion de l'ordre des écritures== Démarrer une nouvelle instruction par cycle est la base du pipeline. Mais avec des instructions multi-cycles, un problème bien particulier survient. Le processeur exécute plusieurs instructions en même temps, dans le même étage d’exécution, mais dans des ALUS/multiplieurs/FPU séparés. Le problème, c'est que si les instructions démarrent dans l'ordre du programme, les unes après les autres, elles peuvent terminer dans le désordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé ! Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre. On dit qu'il y a '''inversion de l'ordre des écritures'''. Et cette inversion cause de nombreux problèmes. Premier problème : des registres peuvent avoir une valeur invalide. Si les écritures changent d'ordre, une instruction peut lire un opérande qui n'est pas la bonne, qui n'a pas été écrite par la bonne instruction. Le programme ne s'exécute pas correctement. De plus, les mécanismes d'exception précises cessent de fonctionner, car ils supposent que les écritures dans les registres se font dans l'ordre. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Maintenant, autre problème, qui peut être vu comme un cas particulier du précédent. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Il y a un phénomène de '''double écriture simultanées'''. Et s'il s'agit là d'un exemple qui mélange dépendance structurelle et dépendance WAW. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! ... |- | Multiplication || Multiplication || Multiplication ||class="f_bleu" | Écriture || || |- | || Addition || Addition || class="f_bleu" | Ecriture || |} Il y a deux solutions principales pour régler ce problème, avec une troisième solution spécialement adaptée aux instructions mémoire, qui sont à part des autres. La première méthode permet d'utiliser des instructions multi-cycles, avec un pipeline fixe. La seconde utilise soit un pipeline fixe, soit un pipeline dynamique, mais avec des restrictions assez importantes. La gestion des défauts de cache est elle un cas à part, valable sur tous les types de pipelines. ===Les registres d'échelonnement=== La première solution est assez brute de décoffrage. Elle force toutes les instructions à prendre exactement le même nombre de cycles d'horloge. Elle utilise donc un pipeline où toutes les instructions ont le même nombre d'étages. Un tel pipeline est appelé un ''pipeline de longueur fixe'', ou encore un '''pipeline fixe'''. Et le processeur se cale sur les instructions les plus lentes. Par exemple, si une addition prend un cycle d'horloge, qu'une multiplication en prend 8 et qu'un décalage en prend 2, alors toutes les instructions prendront 8 cycles d'horloge. Et ces 8 cycles d'horloge correspondront à 8 étages d'exécution. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] Un défaut de la méthode précédente est que les données sont copiées d'un registre de pipeline au suivant à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs ''in order'' modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé. [[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]] ===Les blocages de pipeline=== Une solution alternative n'utilise ni registres d'échelonnage, ni étages qui ne font rien. Elle utilise à la place un '''pipeline dynamique''', de taille variable selon l'instruction. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. Les instructions multi-cycles prennent le temps qu'il faut pour faire leur calcul, les additions donnent leur résultat en un seul cycle, bref : les instructions ont des temps d'exécution variables. On ne se cale pas sur la plus lente. De plus, les instructions écrivent dans les registres au plus tôt, dès qu'elles ont fini leur calcul. [[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]] Pour éviter d'inverser les écritures, ce sont les instructions qui sont retardées. Pour comprendre comment ça marche, prenons l'exemple d'une multiplication suivie d'une addition, encore. La multiplication prend 8 cycles et occupe l'étage d'exécution pendant 8 cycles consécutifs. Pendant ces 8 cycles, l'instruction suivante attend dans l'unité de décodage, elle n'entre pas dans le chemin de donnée. Sa progression dans le pipeline est stoppée, et même que toutes les instructions précédentes. Elle ne redémarre que quand cela garantit que son écriture se fait après l'écriture de la multiplication. [[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]] Cette solution ''retarde les instructions'' pour qu'elles écrivent leurs résultats dans l'ordre. En clair, on retarde la seconde instruction histoire de remettre les écritures dans l'ordre. Lors de cette mise en attente, le pipeline est bloqué, mais seulement pour les étages précédant l'unité d'émission. Il s'agit donc d'un '''blocage sélectif''' du pipeline,n un blocage partiel. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Une implémentation basique bloque le pipeline quand une instruction multi-cycles est en cours d'exécution. Pour cela, l'unité d'émission contient un compteur, qu'il initialise quand il démarre une micro-opération multi-cycles. Le compteur est initialisé avec le nombre de cycles d'horloge passés dans l'unité de calcul. Une nouvelle micro-opération peut démarrer si ce compteur est à zéro, le pipeline est bloqué sinon. Cependant, les performances sont grandement en-deças de ce qu'on pourrait espérer. En effet, la micro-opération candidate est émise avec un retard largement plus grand que nécessaire. Par exemple, imaginons la situation illustrée dans le tableau ci-dessous. Une multiplication, suivie d'une addition qui écrase ce résultat au cycle suivant. Cas complètement irréaliste, mais passons. L'addition prend deux cycles, la multiplication en prend 5. Sans pipeline, voici ce que ça donne : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || Addition || Addition || class="f_bleu" | Écriture || |} Maintenant, ajoutons un pipeline, et utilisons l'unité d'émission décrite plus haut, qui retarde la seconde instruction tant que la première n'est pas terminée. Avec cette implémentation basique, l'addition est retardée tant que la multiplication n'a pas écrit son résultat dans le registre. Les cycles de retard sont indiqués en rouge. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Alors qu'en réalité, une solution optimale serait celle-ci : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Pour obtenir le cas optimal, le processeur utilise un '''''Result Shift Register'''''. Il s'agit d'un registre à décalage, dont le nombre de bits se cale sur l'instruction la plus longue. Par exemple, si le processeur exécute des additions en deux cycles, des décalages en trois cycles et des multiplications en 6 cycles, alors le ''result shift register'' aura 6 bits. Fait très important : on suppose que l'étage de ''writeback'' est comptée dans ces cycles. Et il en est de même pour les étages situés après ou avant l'étage d'exécution, du moment qu'ils suivent l'étage d'émission/le décodeur. Quand une instruction est émise, elle réserve le cycle d'horloge où elle compte faire une écriture. Les autres instructions ne pourront pas faire d'écriture lors d'un cycle réservé. Cela résout le problème des ''double écriture simultanées''. Pour réserver un cycle d'horloge, elle met le bit correspondant à 1, dans le ''result shift register''. Et on peut étendre la méthode pour gérer les dépendances WAW. Une instruction future ne doit pas faire d'écriture avant la micro-opération candidate. Pour cela, elle va empêcher les futures instructions de réserver les cycles précédents, ceux avant le cycle réservé. En clair : elle met les bits correspondant à 1. S'ils le sont déjà, ce n'est pas un problème : ils étaient à 1, ils restent à 1. les réservations faites par les instructions passées ne sont pas écrasées. Ainsi, une instruction qui s'exécute en N cycles réservera N étages de pipeline, N cycles consécutifs. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || 1 || 1 || 1 || 1 |} À chaque cycle d'horloge, ce registre est décalé d'un cran vers la droite. Cela permet de prendre en compte le fait que chaque instruction progresse d'un étage à chaque cycle d'horloge. Il va de soi que ce décalage est un décalage logique, qui remplit les vides par des zéros. Lorsque l'unité d'émission reçoit une instruction, elle vérifie le nombre de cycles qu'elle prend pour s’exécuter. Le nombre de cycles en question permet de savoir quel bit consulter dans le ''result shift''. Pour une instruction qui prend N cycles, elle vérifie si le bit numéro N de ce registre (en partant de la droite). S’il est à 1, cela signifie qu'une autre instruction a déjà réservé cet étage de pipeline. L'instruction candidate va devoir attendre que cet étage soit libre. Si ce bit est à 0, l’unité réserve alors le cycle associé en mettant le bit à 1, ainsi que tous les bits précédents, puis elle est émise. Par exemple, si je prends le cas du ''result shift register'' du tableau précédent. Imaginons qu'une instruction veuille s’exécuter pendant 4 cycles. L'unité d'émission regarde le quatrième bit. Elle voit qu'il est à 1, ce qui fait que ce n'est pas possible. Elle est alors retardée de un cycle. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 1 || 1 || 1 || 1 |} Après le retard, le ''result shift register'' a été décalé, ce qui donne : {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 0 || 1 || 1 || 1 |} Le bit associé vaut 0, ce qui fait que l'instruction peut être émise. Elle met alors ce bit à 1, ainsi que tous les précédents (ils le sont déjà). ===La solution des processeurs MIPS=== Une solution alternative a été utilisée sur les premiers processeurs MIPS. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications. Les multiplications étaient les seules instructions multi-cycles sur ces processeurs. Les instructions multi-cycles disposaient de leur propre registres rien que pour elles. Précisément, les instructions de multiplication enregistraient leur résultat dans des registres LO et HI, pas dans les registres généraux. Le fait de séparer les registres résout ce problème d'inversion des écritures. L'inversion des écritures survient quand on mixe des instructions avec des temps d'exécution différents, mais qui écrivent dans les mêmes registres. Ici, on sépare les registres et les instructions suivant leurs temps d'exécution. Les multiplications étaient émises dans l'ordre, avaient toute le même temps d'exécution : les résultats arrivaient forcément en ordre dans les registres HI/LO. Idem pour les autres instructions qui prenaient un seul cycle : elles étaient émises dans l'ordre, prenaient toutes un cycle, et enregistraient leurs résultats dans l'ordre dans les registres généraux. ==Les dépendances d'instruction== Les problèmes liés aux instructions multi-cycles sont des cas particuliers d'un problème plus général, que tous les pipelines ont. Un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée. Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, les instructions ont des dépendances très variées. Les dépendances qui vont nous intéresser sont les '''dépendances de données'''. Nous avons déjà parlé des '''dépendances structurelles''' au chapitre précédent, aussi je ne reviendrais pas dessus. Il y a aussi les '''dépendances de contrôle''' sont le fait des branchements, interruptions et exceptions, qui fait l'objet des chapitres sur la prédiction de branchement, et d'une section sur les exceptions précises. ===Les dépendances de données : registres et adresses=== Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c’est-à-dire au même registre ou la même adresse mémoire. Et cela nous amène à distinguer deux types de dépendances : de registre, d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité n'est pas réservée aux pipelines complexes. Nous verrons d'ailleurs un exemple frappant dans la fin de ce chapitre. Mais il faut avouer que les dépendances d'adresse ne posent problème que sur les processeurs dotés d'exécution dans le désordre, qui seront vus dans plusieurs chapitres. Aussi, pour le moment, concentrons-nous sur les dépendances de registres. Les dépendances de registre surviennent quand deux instructions lisent ou écrivent un registre. Deux instructions, deux types d'accès (lecture ou écriture), cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent le même registre, mais pas en même temps. * '''Read after write''' : Une instruction lit le résultat écrit par l'autre. * '''Write after write''' : Deux instructions écrivent dans le même registre, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances sont bloquantes. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Concrètement, elles n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW existent à condition que le pipeline supporte les écritures dans le désordre. Or, nous avons vu plus haut comment remettre les écritures dans l'ordre. Les pipelines que nous avons abordés ne sont pas concernés par les dépendances WAW. Aussi, nous repoussons leur étude aux processeurs avec exécution dans le désordre. ===L'unité d’émission : la détection des dépendances de registres=== Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions, ce qui peut être fait après l'unité de décodage. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites, qui sont souvent calculées/déterminées à l'exécution. Les deux types de dépendances sont donc le fait de circuits séparés, dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire, les autres dépendances sont gérées dans une '''unité d'émission''', séparée du décodeur d'instruction, qui a son propre étage de pipeline. L'unité d'émission détecte les dépendances de registres et met en attente les micro-opérations si besoin. Pour simplifier, c'est elle bloque le pipeline quand c'est nécessaire. Et mine de rien, la section sur les instructions multi-cycle nous a montré son rôle. C'est elle qui gèle le pipeline lors d'une instruction multi-cycle. Par contre, elle ne gère pas les défauts de cache, qui sont gérés par l'unité mémoire. {|class="wikitable" |+ Pipeline basique, avec une unité d'émission |- ! Calcul du ''Program Counter'' !! Chargement !! Décodage !! class="f_rouge" | Émission !! Lecture des opérandes (registres) !! Exécution (ALU) !! ''Writeback'' |- | rowspan="2" | ''Program Counter'' | rowspan="2" | Cache d'instruction | rowspan="2" | Décodeur d'instruction | rowspan="2" class="f_rouge" | Unité d'émission | rowspan="2" | Port de lecture du banc de registre (registres) | Unités de calcul | rowspan="2" | Port d'écriture du banc de registre |- | class="f_rouge" | Unité mémoire |} À chaque cycle, l'unité d'émission reçoit une micro-opération et analyse ses dépendances de données. Si elle a une dépendance bloquante, elle est mise en attente dans l'étage d'émission, en attendant que les conditions soient réunies pour pouvoir s'exécuter. Durant ce temps d'attente, le pipeline est bloqué. Sinon, l'unité d'émission la laisse passer à l'étage suivant. Commençons par étudier les dépendances RAW, où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction qui produit le résultat est appelée l''''instruction productrice''', alors que celle qui l'utilise est appelée l''''instruction consommatrice'''. Les deux instructions ont alors une dépendance de donnée, dite de type '''''Read After Write'''''. Et cette dépendance impose que l'instruction productrice soit totalement terminée avant qu'on émette l'instruction consommatrice. La raison est que le résultat de l'instruction productrice doit être enregistré dans les registres, avant d'être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Du fait de ce délai, il se peut que l'instruction consommatrice lise le registre avant que le résultat soit enregistré. Si l'instruction consommatrice est lancée immédiatement après l'instruction productrice, ça arrivera avec certitude. Pour éviter cela, il suffit de retarder l'exécution de l'instruction consommatrice tant que le résultat de l'instruction productrice n'est pas disponible, pas encore dans les registres. Et il se peut qu'elle soit retardée durant quelques cycles pour cela. Et ce retard implique, vous l'aurez deviné, de bloquer le pipeline. Précision importante : les dépendances RAW ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. Mais comment le processeur détecte les dépendances de registres entre deux instructions ? La réponse est simple : il compare les registres utilisés par les deux instructions. Les registres étant connus dès la sortie de l'étape de décodage, l'unité de décodage peut les comparer. L'unité d'émission reçoit une micro-opération décodée, appelée '''micro-opération candidate'''. Elle détecte si cette micro-opération candidate a une dépendance avec les micro-opérations qui sont dans le pipeline, dans les étages suivant l'unité d'émission. Les micro-opérations en question seront appelées '''micro-opérations "en vol"'''. L'unité d'émission compare les registres utilisés par la micro-opération candidate et les micro-opérations en vol. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Une dépendance RAW a lieu entre une instruction consommatrice et une instruction productrice. L'instruction productrice écrit son résultat dans un registre, son registre de destination. L'instruction consommatrice utilise ce résultat comme opérande, ce qui fait que le résultat est dans un de ses registres opérande. Une dépendance RAW implique que l'instruction productrice est en vol, alors que l'instruction consommatrice est candidate. L'unité d'émission doit donc comparer les registres opérandes de l'instruction candidate, avec les registres de destination des instructions en vol. L'unité d'émission doit donc connaitre les registres de destination des micro-opérations en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons. [[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]] Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être lu par une autre instruction. Les registres qui ne sont pas réservés sont dits libres ou 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é, ce qui implique que c'est le registre de destination d'une instruction en vol. L'instruction n'est pas émise et le pipeline est bloqué. Le registre encode les registres réservés en représentation ''one-hot'' modifiée. 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. Un registre est réservé en mettant le bit associé à 1. {|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 |} Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''. 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 l'instruction, 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 l'instruction 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. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. 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 l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul. Diverses optimisations permettent de mitiger l'impact des dépendances RAW. Et dans la section suivante, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. ==Le contournement : réduire les dépendances== Nous venons de voir que les dépendances imposent de bloquer le pipeline, pendant quelques cycles. Et encore une fois, c'est l'unité d'émission qui s'en charge. Du moins, c'est le cas sans optimisation. Une optimisation permet de fortement réduire les blocages de pipelines liés aux dépendances RAW, jusqu'à les annuler dans certains cas. L'optimisation en question est celle du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] Le contournement fait que deux instructions consécutives avec une dépendance RAW peuvent quand même être émises. Mais comme on va le voir, cela n'élimine pas les blocages de pipelines liés aux dépendances RAW. L'unité d'émission existe toujours, elle n'est pas forcément simplifiée, au contraire. Par contre, les blocages de pipeline sont moins fréquents, et les performances augmentent. : Avant de poursuivre, rappelons le vocabulaire. Si une instruction produit un résultat, qui utilisé par une seconde instruction comme opérande : la première instruction est appelée l''''instruction productrice''', la seconde est appelée l''''instruction consommatrice'''. ===Le contournement de l'unité de calcul=== La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée. [[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]] Le résultat d'une opération est mémorisé en sortie de l'ALU, et précisément dans le registre de pipeline, au cycle suivant. Le contournement connecte ce registre de pipeline sur l'entrée de l'ALU si besoin, et on le déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs. [[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]] Intuitivement, on se dit que c'est l'unité d'émission qui commande le multiplexeur. Mais il est possible de déporter cette fonction en-dehors de l'unité d'émission, de gérer cela au niveau "local". La commande du multiplexeur est réalisée par un circuit situé pas loin de l'ALU elle-même. L'unité d'émission émet juste deux micro-opérations consécutives qui ont une dépendance RAW, à condition que le résultat de l'une soit immédiatement consommé par l'autre, et que les deux utilisent l'ALU entière. Pour commander les multiplexeurs, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul. [[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]] La présence de plusieurs unités de calcul complexifie le contournement. Un processeur a typiquement une ALU entière, un ''barrel shifter'' et un multiplieur entier. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque circuit à la fois sur son entrée, mais aussi sur l'entrée des deux autres. En tout, cela demande 9 interconnexions, avec autant de multiplexeurs. Et c'est la même chose à l'intérieur d'une FPU. Pour rappel une FPU est typiquement composée de deux circuits : un additionneur flottant et un multiplieur flottant. Non seulement ils ont leur sortie connectée à leur entrée, mais en plus, la sortie de l'un est relié à l'entrée de l'autre. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand-chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies entre registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un entre FPUs. [[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]] Il faut aussi penser au cas où les ALU entières calculent des adresses mémoire. Dans ce cas, il faut aussi connecter une unité de calcul à l'unité mémoire. Le ''barrel shifter'' et le multiplieur ne sont pas concernés, seule l'ALU entière capable de faire des additions l'est. La connexion est unidirectionnelle, de l'ALU entière vers l'unité mémoire. Si l'unité mémoire incorpore sa propre unité de calcul, il faut faire de même, mais pas pour les mêmes raisons. Le réseau de contournement ne transmet alors pas des adresses mémoire calculées, mais les opérandes de ces calculs. Ces opérandes viennent soit des registres, soit sont calculées dans l'ALU entière. Mais le réseau de contournement est alors plus complexe, car toutes les ALU entières peuvent calculer ces opérandes. En pratique, c'est rarement fait. L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires. ===Le contournement des lectures mémoire=== La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand une lecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué). Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre. Et cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire. [[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]] La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs. [[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]] Ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt. [[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]] La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux. [[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]] La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour. ===Le contournement des instructions multi-cycles=== La durée des instructions est à prendre en compte pour effectuer un contournement convenable. Le contournement entre instructions simples est facile, vu qu'elles s'exécutent en un seul cycle. Leur résultat peut être envoyé immédiatement sur leur entrée. Il n'y a aucun besoin de bloquer le pipeline. Mais avec les instructions multi-cycles, les choses sont plus compliquées, et cela mérite un exemple explicatif. Un exemple est celui où une multiplication de 5 cycles est suivie par une addition de deux cycles. Si les deux instructions sont indépendantes, la seconde sera retardée, en raison des dépendances WAW, mais pas à cause des dépendances RAW. Voici comment sont exécutées les deux instructions : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Maintenant, supposons que l'addition a pour opérande le résultat de la multiplication. On voit que l'on ne peut pas reprendre tel quel le tableau précédent, car l'addition serait émise trop tôt. Il faut donc retarder encore l'addition, pour qu'elle démarre quand la multiplication vient tout juste de terminer. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} L'unité d'émission retarde l'émission d'une instruction pendant quelques cycles, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas. Le contournement doit aussi tenir compte des registres d'échelonnage. Il arrive en effet qu'une instruction soit émise alors que son opérande est encore dans les registres d'échelonnage. Ce n'est pas rare, c'est même assez fréquent. Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Mais lire les opérandes dans les registres d'échelonnage nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin. ===L’émission anticipée des micro-opérations=== Idéalement, on voudrait démarrer une nouvelle micro-opération sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU. Et cela peut poser problème pour le contournement. Imaginons que l'on ait trois cycles entre l'unité d'émission et les ALU. Si une instruction est émise quand son opérande sort de l'ALU, elle prend plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''. Le nombre de cycles d'avance est égal au nombre d'étages entre l'unité de calcul et l'unité d'émission. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Et cela nous amène à la section suivante. Exploiter le contournement de l'unité mémoire demande de savoir combien de temps la lecture va prendre. Et le processeur n'a aucun moyen de le savoir. A la place, il spécule, comme il le fait avec la prédiction de branchement. Le processeur part du principe que l'accès mémoire fera un succès de cache, et émet les instructions lectures-dépendantes avec ce délai. Mais en cas de défaut de cache, les instructions lecture-dépendantes lisent des opérandes invalides. Et le processeur doit corriger le tir. Et pour cela, il a plusieurs solutions. Ce qui nous amène à la section suivante. ==Les défauts de cache bloquent le pipeline== Une instruction mémoire a un temps d'exécution variable, en raison des défauts de cache. En général, elle fait un succès de cache et accède au cache L1 de données. L'accès mémoire se fait en un cycle, parfois 2 cycles si le cache est pipeliné. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : une dizaine de cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation. Et si le défaut de cache se double d'un défaut de page mémoire, l'instruction peut dépasser le millier de cycles d'horloge. Les pipelines précédents doivent donc gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Un problème est que les accès mémoire ne peuvent pas être gérés comme les autres instructions multi-cycles, car ce sont des instructions au temps d'exécution variable et imprédictible. Et cela pose des problèmes sur plusieurs points. Premièrement, l'émission anticipée des micro-opérations est impactée, surtout pour contourner les lectures. Deuxièmement, cela pose des problèmes en matière d'ordre des écritures, pour les lectures et les écritures. ===Les défauts de cache en lecture causent des inversions d'écritures=== Lors d'un défaut de cache, il faut distinguer les lectures des écritures. Les écritures sont gérés par la file d'écriture, ce qui fait que si défaut de cache il y a, c'est une fois que l'écriture quitte la file d'écriture, quand l'instruction associée quitte le pipeline. Le défaut de cache peut alors être traité alors que le processeur continue d'exécuter des instructions dans son pipeline. Le processeur n'est pas bloqué, le système d'exception précise n'est pas impliqué. Par contre, pour les défauts de cache des lectures, les choses sont différentes. Les lectures lisent une donnée et l'écrivent dans un registre, son registre de destination. Elles écrivent donc dans les registres. Et les problèmes d'ordres des écritures peuvent survenir, en cas de défaut de cache. Si une lecture fait un défaut de cache, il faudra attendre plusieurs cycles avant qu'elle écrive dans le registre de destination. Mais les instructions suivant la lecture vont continuer à s’exécuter, voire écrire dans les registres. Un défaut de cache dure longtemps, une dizaine de cycles, voire plus. C'est souvent plus de cycles que la totalité du pipeline du processeur. Si les instructions après la lecture continuaient leur exécution, elles finiraient par atteindre la fin du pipeline. Le résultat est que ces instructions quitteraient le pipeline, écriraient dans les registres, voire dans la mémoire RAM, et ce serait définitif. Elles écriront dans les registres avant que la lecture le fasse, alors que c'est censé être l'inverse : il y a inversion de l'ordre des écritures. De plus, si la lecture déclenche une exception matérielle, on ne pourrait pas revenir en arrière, le système d'exception précise serait en erreur. Un autre défaut, lié au précédent, est que les instructions lecture-dépendantes sont émises en avance, en supposant que la lecture fait un défaut de cache. En cas de défaut de cache, elles liront un opérande invalide. Et ces instructions invalides doivent être annulées totalement. ===Le blocage du pipeline par l'unité mémoire=== Les processeurs ''in-order'' disposent de deux solutions pour corriger ces problèmes. La première utilise le système d'exceptions précises. Un défaut de cache est traité comme une exception matérielle un peu particulière. Lorsque l'unité mémoire détecte un défaut de cache, elle marque la donnée lue comme invalide et lève une exception matérielle "défaut de cache". Lorsque cette exception atteint l'étage de ''writeback'', celui-ci annule toutes les instructions qui suivent. Le processeur reprend son exécution à la lecture elle-même, éventuellement après un temps d'attente, pour tenir compte de la latence du cache L2, ou de la mémoire RAM. La seconde solution est de bloquer totalement le pipeline. Pour geler le pipeline, l'unité mémoire génère un signal ''pipeline stall''. Il est mis à 1 pendant qu'un défaut de cache est en cours, il est remis à zéro une fois le défaut de cache terminé. Ce signal est utilisé pour geler les registres du pipeline, dans les étages précédents. Seuls les étages situés avant l'étage MEM sont gelés. Ceux situés après peuvent faire leur travail normalement, ce qui permet au système d'exceptions précises de faire son travail. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé. [[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]] La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de cache est en cours. C'est seulement quand le défaut est terminé et qu'il reçoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline. ===Les optimisations : ne pas bloquer totalement le pipeline en cas de défaut de cache=== Bloquer le pipeline est une solution extrême. En théorie, il est possible de continuer à exécuter des instructions après un défaut de cache, sous certaines conditions. Il suffit juste que les instructions en question n'écrivent pas dans les registres. C'est même ce que font les processeurs à exécution dans le désordre. Ils ne sont pas bloqués après un défaut de cache. Cependant, ils peuvent le faire car ils ont un système d'exception précise élaboré, pas les processeurs ''in-order''. Néanmoins, cela suggère une optimisation. L'idée est de continuer à exécuter des instructions après le défaut de cache. L'unité mémoire est bloquée, elle ne peut pas accepter de nouvelle instruction, mais l'ALU entière, la FPU et les autres circuits sont utilisables. Les instructions émises après le défaut de cache sont alors appelées les ''instructions en avance''. Elles n'ont pas le droit d'enregistrer leurs résultats, que ce soit dans les registres ou la RAM. Pour cela, les micro-opérations restent dans le pipeline et sont stoppées dans le dernier étage libre. Par exemple, imaginons qu'un défaut de cache ait lieu à l'instant T, au cycle T. La lecture va se propager dans le pipeline jusqu'à l'étage de ''writeback'', dont à la toute fin du pipeline. La micro-opération suivante, émise au cycle T+2, s’arrêtera à l'étage d'avant. La micro-opération encore avant s’arrêtera à l'étage encore avant, et ainsi de suite. Le pipeline finira par se remplir totalement, et l'unité d'émission n'aura pas le choix que de bloquer le pipeline. Le résultat est que des micro-opérations auront été préchargées et exécutées à l'avance, mais ne se seront pas "terminées". Il y a toujours la possibilité de les annuler, en cas de problèmes. Le nombre de micro-opérations chargées en avance est égal au nombre d'étage du pipeline, vu qu'il est totalement remplit. Cette optimisation a possiblement été utilisée par les processeurs ARM de marque Cortex A53, A55 et A510, bien que ce ne soit pas certain (il s'agit d'une supposition fite par l'auteur du bloc "Chips and cheese". Le Cortex A52 et le A55 pouvaient exécuter en avance 8 instructions, et ils avaient un pipeline de 8 étages, alors que le A510 pouvait avoir 12 instructions en avance, et il avait justement un pipeline de 12 étages. {|class="wikitable" |- ! Fetch 1 !! Fetch 2 !! Décodage 1 !! Décodage 2 !! Émission !! Lecture des registres !! Exécution !! ''Writeback'' |- | Micro-opération T + 8 || Micro-opération T + 7 || Micro-opération T + 6 || Micro-opération T + 5 || Micro-opération T + 4 || Micro-opération T + 3 || Micro-opération T + 2 || Micro-opération T + 1 || colspan="8" class="f_rouge" | Lecture, en défaut au cycle T |} <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'émission des instructions | prevText=L'émission des instructions | next=Le scoreboard et le ROB | nextText=Le scoreboard et le ROB }} </noinclude> 2avi97nod3jl38wsg6pjn3zndi4ampf 775085 775084 2026-10-12T20:20:47Z Mewtow 31375 /* Les défauts de cache bloquent le pipeline */ 775085 wikitext text/x-wiki Avec ce qu'on a dit dans le chapitre précédent, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipeline doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on va expliquer dans ce chapitre. Dans ce chapitre, nous allons voir les '''processeurs ''in-order'''''. Les définir n'est pas facile, surtout à ce stade du cours. Ils sont opposés aux processeurs avec exécution dans le désordre, qu'on verra dans quelques chapitres. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. On démarre les instructions une par une, dans un certain ordre. Mais le parcours du pipeline n'est pas un long fleuve tranquille, comme on le verra dans ce chapitre, et le résultat à la fin du pipeline est parfois différent. À la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Au final, deux instructions peuvent théoriquement arriver dans le désordre. Du moins, ce serait le cas si le processeur n'était pas conçu pour corriger le problème. Les processeurs ''in-order'' garantissent que des instructions chargées dans un certain ordre termineront dans le même ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner au mieux. Les ''processeurs à exécution dans le désordre'', eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance. Mais ils ont des systèmes pour éviter que cela pose problème. Ils utilisent pour cela des systèmes d'exception précises plus élaborés que ceux des chapitres précédents. Mais cela sera pour un autre chapitre. Pour ce chapitre, nous allons nous concentrer sur les processeurs ''in-order''. Mais avant toute chose, un petit rappel. Un processeur avec un pipeline est décomposé en deux parties. Le ''front end''), prend en charge les étages communs à toutes les instructions, comme le chargement et le décodage. L'amont est suivi une unité d'émission, elle-même suivie par le chemin de données. Le chemin de données contient plusieurs unités différentes : des unités de calcul, l'unité mémoire, parfois des unités pour les branchements. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] Avec des instructions multi-cycles, le pipeline exploite plusieurs unités en même temps, c'est même la norme. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. Et le reste du chapitre vise à expliquer quelles sont ces contraintes. ==L'inversion de l'ordre des écritures== Démarrer une nouvelle instruction par cycle est la base du pipeline. Mais avec des instructions multi-cycles, un problème bien particulier survient. Le processeur exécute plusieurs instructions en même temps, dans le même étage d’exécution, mais dans des ALUS/multiplieurs/FPU séparés. Le problème, c'est que si les instructions démarrent dans l'ordre du programme, les unes après les autres, elles peuvent terminer dans le désordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé ! Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre. On dit qu'il y a '''inversion de l'ordre des écritures'''. Et cette inversion cause de nombreux problèmes. Premier problème : des registres peuvent avoir une valeur invalide. Si les écritures changent d'ordre, une instruction peut lire un opérande qui n'est pas la bonne, qui n'a pas été écrite par la bonne instruction. Le programme ne s'exécute pas correctement. De plus, les mécanismes d'exception précises cessent de fonctionner, car ils supposent que les écritures dans les registres se font dans l'ordre. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Maintenant, autre problème, qui peut être vu comme un cas particulier du précédent. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Il y a un phénomène de '''double écriture simultanées'''. Et s'il s'agit là d'un exemple qui mélange dépendance structurelle et dépendance WAW. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! ... |- | Multiplication || Multiplication || Multiplication ||class="f_bleu" | Écriture || || |- | || Addition || Addition || class="f_bleu" | Ecriture || |} Il y a deux solutions principales pour régler ce problème, avec une troisième solution spécialement adaptée aux instructions mémoire, qui sont à part des autres. La première méthode permet d'utiliser des instructions multi-cycles, avec un pipeline fixe. La seconde utilise soit un pipeline fixe, soit un pipeline dynamique, mais avec des restrictions assez importantes. La gestion des défauts de cache est elle un cas à part, valable sur tous les types de pipelines. ===Les registres d'échelonnement=== La première solution est assez brute de décoffrage. Elle force toutes les instructions à prendre exactement le même nombre de cycles d'horloge. Elle utilise donc un pipeline où toutes les instructions ont le même nombre d'étages. Un tel pipeline est appelé un ''pipeline de longueur fixe'', ou encore un '''pipeline fixe'''. Et le processeur se cale sur les instructions les plus lentes. Par exemple, si une addition prend un cycle d'horloge, qu'une multiplication en prend 8 et qu'un décalage en prend 2, alors toutes les instructions prendront 8 cycles d'horloge. Et ces 8 cycles d'horloge correspondront à 8 étages d'exécution. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] Un défaut de la méthode précédente est que les données sont copiées d'un registre de pipeline au suivant à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs ''in order'' modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé. [[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]] ===Les blocages de pipeline=== Une solution alternative n'utilise ni registres d'échelonnage, ni étages qui ne font rien. Elle utilise à la place un '''pipeline dynamique''', de taille variable selon l'instruction. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. Les instructions multi-cycles prennent le temps qu'il faut pour faire leur calcul, les additions donnent leur résultat en un seul cycle, bref : les instructions ont des temps d'exécution variables. On ne se cale pas sur la plus lente. De plus, les instructions écrivent dans les registres au plus tôt, dès qu'elles ont fini leur calcul. [[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]] Pour éviter d'inverser les écritures, ce sont les instructions qui sont retardées. Pour comprendre comment ça marche, prenons l'exemple d'une multiplication suivie d'une addition, encore. La multiplication prend 8 cycles et occupe l'étage d'exécution pendant 8 cycles consécutifs. Pendant ces 8 cycles, l'instruction suivante attend dans l'unité de décodage, elle n'entre pas dans le chemin de donnée. Sa progression dans le pipeline est stoppée, et même que toutes les instructions précédentes. Elle ne redémarre que quand cela garantit que son écriture se fait après l'écriture de la multiplication. [[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]] Cette solution ''retarde les instructions'' pour qu'elles écrivent leurs résultats dans l'ordre. En clair, on retarde la seconde instruction histoire de remettre les écritures dans l'ordre. Lors de cette mise en attente, le pipeline est bloqué, mais seulement pour les étages précédant l'unité d'émission. Il s'agit donc d'un '''blocage sélectif''' du pipeline,n un blocage partiel. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Une implémentation basique bloque le pipeline quand une instruction multi-cycles est en cours d'exécution. Pour cela, l'unité d'émission contient un compteur, qu'il initialise quand il démarre une micro-opération multi-cycles. Le compteur est initialisé avec le nombre de cycles d'horloge passés dans l'unité de calcul. Une nouvelle micro-opération peut démarrer si ce compteur est à zéro, le pipeline est bloqué sinon. Cependant, les performances sont grandement en-deças de ce qu'on pourrait espérer. En effet, la micro-opération candidate est émise avec un retard largement plus grand que nécessaire. Par exemple, imaginons la situation illustrée dans le tableau ci-dessous. Une multiplication, suivie d'une addition qui écrase ce résultat au cycle suivant. Cas complètement irréaliste, mais passons. L'addition prend deux cycles, la multiplication en prend 5. Sans pipeline, voici ce que ça donne : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || Addition || Addition || class="f_bleu" | Écriture || |} Maintenant, ajoutons un pipeline, et utilisons l'unité d'émission décrite plus haut, qui retarde la seconde instruction tant que la première n'est pas terminée. Avec cette implémentation basique, l'addition est retardée tant que la multiplication n'a pas écrit son résultat dans le registre. Les cycles de retard sont indiqués en rouge. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Alors qu'en réalité, une solution optimale serait celle-ci : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Pour obtenir le cas optimal, le processeur utilise un '''''Result Shift Register'''''. Il s'agit d'un registre à décalage, dont le nombre de bits se cale sur l'instruction la plus longue. Par exemple, si le processeur exécute des additions en deux cycles, des décalages en trois cycles et des multiplications en 6 cycles, alors le ''result shift register'' aura 6 bits. Fait très important : on suppose que l'étage de ''writeback'' est comptée dans ces cycles. Et il en est de même pour les étages situés après ou avant l'étage d'exécution, du moment qu'ils suivent l'étage d'émission/le décodeur. Quand une instruction est émise, elle réserve le cycle d'horloge où elle compte faire une écriture. Les autres instructions ne pourront pas faire d'écriture lors d'un cycle réservé. Cela résout le problème des ''double écriture simultanées''. Pour réserver un cycle d'horloge, elle met le bit correspondant à 1, dans le ''result shift register''. Et on peut étendre la méthode pour gérer les dépendances WAW. Une instruction future ne doit pas faire d'écriture avant la micro-opération candidate. Pour cela, elle va empêcher les futures instructions de réserver les cycles précédents, ceux avant le cycle réservé. En clair : elle met les bits correspondant à 1. S'ils le sont déjà, ce n'est pas un problème : ils étaient à 1, ils restent à 1. les réservations faites par les instructions passées ne sont pas écrasées. Ainsi, une instruction qui s'exécute en N cycles réservera N étages de pipeline, N cycles consécutifs. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || 1 || 1 || 1 || 1 |} À chaque cycle d'horloge, ce registre est décalé d'un cran vers la droite. Cela permet de prendre en compte le fait que chaque instruction progresse d'un étage à chaque cycle d'horloge. Il va de soi que ce décalage est un décalage logique, qui remplit les vides par des zéros. Lorsque l'unité d'émission reçoit une instruction, elle vérifie le nombre de cycles qu'elle prend pour s’exécuter. Le nombre de cycles en question permet de savoir quel bit consulter dans le ''result shift''. Pour une instruction qui prend N cycles, elle vérifie si le bit numéro N de ce registre (en partant de la droite). S’il est à 1, cela signifie qu'une autre instruction a déjà réservé cet étage de pipeline. L'instruction candidate va devoir attendre que cet étage soit libre. Si ce bit est à 0, l’unité réserve alors le cycle associé en mettant le bit à 1, ainsi que tous les bits précédents, puis elle est émise. Par exemple, si je prends le cas du ''result shift register'' du tableau précédent. Imaginons qu'une instruction veuille s’exécuter pendant 4 cycles. L'unité d'émission regarde le quatrième bit. Elle voit qu'il est à 1, ce qui fait que ce n'est pas possible. Elle est alors retardée de un cycle. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 1 || 1 || 1 || 1 |} Après le retard, le ''result shift register'' a été décalé, ce qui donne : {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 0 || 1 || 1 || 1 |} Le bit associé vaut 0, ce qui fait que l'instruction peut être émise. Elle met alors ce bit à 1, ainsi que tous les précédents (ils le sont déjà). ===La solution des processeurs MIPS=== Une solution alternative a été utilisée sur les premiers processeurs MIPS. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications. Les multiplications étaient les seules instructions multi-cycles sur ces processeurs. Les instructions multi-cycles disposaient de leur propre registres rien que pour elles. Précisément, les instructions de multiplication enregistraient leur résultat dans des registres LO et HI, pas dans les registres généraux. Le fait de séparer les registres résout ce problème d'inversion des écritures. L'inversion des écritures survient quand on mixe des instructions avec des temps d'exécution différents, mais qui écrivent dans les mêmes registres. Ici, on sépare les registres et les instructions suivant leurs temps d'exécution. Les multiplications étaient émises dans l'ordre, avaient toute le même temps d'exécution : les résultats arrivaient forcément en ordre dans les registres HI/LO. Idem pour les autres instructions qui prenaient un seul cycle : elles étaient émises dans l'ordre, prenaient toutes un cycle, et enregistraient leurs résultats dans l'ordre dans les registres généraux. ==Les dépendances d'instruction== Les problèmes liés aux instructions multi-cycles sont des cas particuliers d'un problème plus général, que tous les pipelines ont. Un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée. Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, les instructions ont des dépendances très variées. Les dépendances qui vont nous intéresser sont les '''dépendances de données'''. Nous avons déjà parlé des '''dépendances structurelles''' au chapitre précédent, aussi je ne reviendrais pas dessus. Il y a aussi les '''dépendances de contrôle''' sont le fait des branchements, interruptions et exceptions, qui fait l'objet des chapitres sur la prédiction de branchement, et d'une section sur les exceptions précises. ===Les dépendances de données : registres et adresses=== Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c’est-à-dire au même registre ou la même adresse mémoire. Et cela nous amène à distinguer deux types de dépendances : de registre, d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité n'est pas réservée aux pipelines complexes. Nous verrons d'ailleurs un exemple frappant dans la fin de ce chapitre. Mais il faut avouer que les dépendances d'adresse ne posent problème que sur les processeurs dotés d'exécution dans le désordre, qui seront vus dans plusieurs chapitres. Aussi, pour le moment, concentrons-nous sur les dépendances de registres. Les dépendances de registre surviennent quand deux instructions lisent ou écrivent un registre. Deux instructions, deux types d'accès (lecture ou écriture), cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent le même registre, mais pas en même temps. * '''Read after write''' : Une instruction lit le résultat écrit par l'autre. * '''Write after write''' : Deux instructions écrivent dans le même registre, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances sont bloquantes. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Concrètement, elles n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW existent à condition que le pipeline supporte les écritures dans le désordre. Or, nous avons vu plus haut comment remettre les écritures dans l'ordre. Les pipelines que nous avons abordés ne sont pas concernés par les dépendances WAW. Aussi, nous repoussons leur étude aux processeurs avec exécution dans le désordre. ===L'unité d’émission : la détection des dépendances de registres=== Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions, ce qui peut être fait après l'unité de décodage. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites, qui sont souvent calculées/déterminées à l'exécution. Les deux types de dépendances sont donc le fait de circuits séparés, dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire, les autres dépendances sont gérées dans une '''unité d'émission''', séparée du décodeur d'instruction, qui a son propre étage de pipeline. L'unité d'émission détecte les dépendances de registres et met en attente les micro-opérations si besoin. Pour simplifier, c'est elle bloque le pipeline quand c'est nécessaire. Et mine de rien, la section sur les instructions multi-cycle nous a montré son rôle. C'est elle qui gèle le pipeline lors d'une instruction multi-cycle. Par contre, elle ne gère pas les défauts de cache, qui sont gérés par l'unité mémoire. {|class="wikitable" |+ Pipeline basique, avec une unité d'émission |- ! Calcul du ''Program Counter'' !! Chargement !! Décodage !! class="f_rouge" | Émission !! Lecture des opérandes (registres) !! Exécution (ALU) !! ''Writeback'' |- | rowspan="2" | ''Program Counter'' | rowspan="2" | Cache d'instruction | rowspan="2" | Décodeur d'instruction | rowspan="2" class="f_rouge" | Unité d'émission | rowspan="2" | Port de lecture du banc de registre (registres) | Unités de calcul | rowspan="2" | Port d'écriture du banc de registre |- | class="f_rouge" | Unité mémoire |} À chaque cycle, l'unité d'émission reçoit une micro-opération et analyse ses dépendances de données. Si elle a une dépendance bloquante, elle est mise en attente dans l'étage d'émission, en attendant que les conditions soient réunies pour pouvoir s'exécuter. Durant ce temps d'attente, le pipeline est bloqué. Sinon, l'unité d'émission la laisse passer à l'étage suivant. Commençons par étudier les dépendances RAW, où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction qui produit le résultat est appelée l''''instruction productrice''', alors que celle qui l'utilise est appelée l''''instruction consommatrice'''. Les deux instructions ont alors une dépendance de donnée, dite de type '''''Read After Write'''''. Et cette dépendance impose que l'instruction productrice soit totalement terminée avant qu'on émette l'instruction consommatrice. La raison est que le résultat de l'instruction productrice doit être enregistré dans les registres, avant d'être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Du fait de ce délai, il se peut que l'instruction consommatrice lise le registre avant que le résultat soit enregistré. Si l'instruction consommatrice est lancée immédiatement après l'instruction productrice, ça arrivera avec certitude. Pour éviter cela, il suffit de retarder l'exécution de l'instruction consommatrice tant que le résultat de l'instruction productrice n'est pas disponible, pas encore dans les registres. Et il se peut qu'elle soit retardée durant quelques cycles pour cela. Et ce retard implique, vous l'aurez deviné, de bloquer le pipeline. Précision importante : les dépendances RAW ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. Mais comment le processeur détecte les dépendances de registres entre deux instructions ? La réponse est simple : il compare les registres utilisés par les deux instructions. Les registres étant connus dès la sortie de l'étape de décodage, l'unité de décodage peut les comparer. L'unité d'émission reçoit une micro-opération décodée, appelée '''micro-opération candidate'''. Elle détecte si cette micro-opération candidate a une dépendance avec les micro-opérations qui sont dans le pipeline, dans les étages suivant l'unité d'émission. Les micro-opérations en question seront appelées '''micro-opérations "en vol"'''. L'unité d'émission compare les registres utilisés par la micro-opération candidate et les micro-opérations en vol. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Une dépendance RAW a lieu entre une instruction consommatrice et une instruction productrice. L'instruction productrice écrit son résultat dans un registre, son registre de destination. L'instruction consommatrice utilise ce résultat comme opérande, ce qui fait que le résultat est dans un de ses registres opérande. Une dépendance RAW implique que l'instruction productrice est en vol, alors que l'instruction consommatrice est candidate. L'unité d'émission doit donc comparer les registres opérandes de l'instruction candidate, avec les registres de destination des instructions en vol. L'unité d'émission doit donc connaitre les registres de destination des micro-opérations en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons. [[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]] Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être lu par une autre instruction. Les registres qui ne sont pas réservés sont dits libres ou 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é, ce qui implique que c'est le registre de destination d'une instruction en vol. L'instruction n'est pas émise et le pipeline est bloqué. Le registre encode les registres réservés en représentation ''one-hot'' modifiée. 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. Un registre est réservé en mettant le bit associé à 1. {|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 |} Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''. 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 l'instruction, 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 l'instruction 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. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. 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 l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul. Diverses optimisations permettent de mitiger l'impact des dépendances RAW. Et dans la section suivante, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. ==Le contournement : réduire les dépendances== Nous venons de voir que les dépendances imposent de bloquer le pipeline, pendant quelques cycles. Et encore une fois, c'est l'unité d'émission qui s'en charge. Du moins, c'est le cas sans optimisation. Une optimisation permet de fortement réduire les blocages de pipelines liés aux dépendances RAW, jusqu'à les annuler dans certains cas. L'optimisation en question est celle du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] Le contournement fait que deux instructions consécutives avec une dépendance RAW peuvent quand même être émises. Mais comme on va le voir, cela n'élimine pas les blocages de pipelines liés aux dépendances RAW. L'unité d'émission existe toujours, elle n'est pas forcément simplifiée, au contraire. Par contre, les blocages de pipeline sont moins fréquents, et les performances augmentent. : Avant de poursuivre, rappelons le vocabulaire. Si une instruction produit un résultat, qui utilisé par une seconde instruction comme opérande : la première instruction est appelée l''''instruction productrice''', la seconde est appelée l''''instruction consommatrice'''. ===Le contournement de l'unité de calcul=== La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée. [[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]] Le résultat d'une opération est mémorisé en sortie de l'ALU, et précisément dans le registre de pipeline, au cycle suivant. Le contournement connecte ce registre de pipeline sur l'entrée de l'ALU si besoin, et on le déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs. [[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]] Intuitivement, on se dit que c'est l'unité d'émission qui commande le multiplexeur. Mais il est possible de déporter cette fonction en-dehors de l'unité d'émission, de gérer cela au niveau "local". La commande du multiplexeur est réalisée par un circuit situé pas loin de l'ALU elle-même. L'unité d'émission émet juste deux micro-opérations consécutives qui ont une dépendance RAW, à condition que le résultat de l'une soit immédiatement consommé par l'autre, et que les deux utilisent l'ALU entière. Pour commander les multiplexeurs, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul. [[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]] La présence de plusieurs unités de calcul complexifie le contournement. Un processeur a typiquement une ALU entière, un ''barrel shifter'' et un multiplieur entier. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque circuit à la fois sur son entrée, mais aussi sur l'entrée des deux autres. En tout, cela demande 9 interconnexions, avec autant de multiplexeurs. Et c'est la même chose à l'intérieur d'une FPU. Pour rappel une FPU est typiquement composée de deux circuits : un additionneur flottant et un multiplieur flottant. Non seulement ils ont leur sortie connectée à leur entrée, mais en plus, la sortie de l'un est relié à l'entrée de l'autre. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand-chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies entre registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un entre FPUs. [[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]] Il faut aussi penser au cas où les ALU entières calculent des adresses mémoire. Dans ce cas, il faut aussi connecter une unité de calcul à l'unité mémoire. Le ''barrel shifter'' et le multiplieur ne sont pas concernés, seule l'ALU entière capable de faire des additions l'est. La connexion est unidirectionnelle, de l'ALU entière vers l'unité mémoire. Si l'unité mémoire incorpore sa propre unité de calcul, il faut faire de même, mais pas pour les mêmes raisons. Le réseau de contournement ne transmet alors pas des adresses mémoire calculées, mais les opérandes de ces calculs. Ces opérandes viennent soit des registres, soit sont calculées dans l'ALU entière. Mais le réseau de contournement est alors plus complexe, car toutes les ALU entières peuvent calculer ces opérandes. En pratique, c'est rarement fait. L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires. ===Le contournement des lectures mémoire=== La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand une lecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué). Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre. Et cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire. [[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]] La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs. [[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]] Ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt. [[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]] La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux. [[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]] La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour. ===Le contournement des instructions multi-cycles=== La durée des instructions est à prendre en compte pour effectuer un contournement convenable. Le contournement entre instructions simples est facile, vu qu'elles s'exécutent en un seul cycle. Leur résultat peut être envoyé immédiatement sur leur entrée. Il n'y a aucun besoin de bloquer le pipeline. Mais avec les instructions multi-cycles, les choses sont plus compliquées, et cela mérite un exemple explicatif. Un exemple est celui où une multiplication de 5 cycles est suivie par une addition de deux cycles. Si les deux instructions sont indépendantes, la seconde sera retardée, en raison des dépendances WAW, mais pas à cause des dépendances RAW. Voici comment sont exécutées les deux instructions : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Maintenant, supposons que l'addition a pour opérande le résultat de la multiplication. On voit que l'on ne peut pas reprendre tel quel le tableau précédent, car l'addition serait émise trop tôt. Il faut donc retarder encore l'addition, pour qu'elle démarre quand la multiplication vient tout juste de terminer. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} L'unité d'émission retarde l'émission d'une instruction pendant quelques cycles, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas. Le contournement doit aussi tenir compte des registres d'échelonnage. Il arrive en effet qu'une instruction soit émise alors que son opérande est encore dans les registres d'échelonnage. Ce n'est pas rare, c'est même assez fréquent. Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Mais lire les opérandes dans les registres d'échelonnage nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin. ===L’émission anticipée des micro-opérations=== Idéalement, on voudrait démarrer une nouvelle micro-opération sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU. Et cela peut poser problème pour le contournement. Imaginons que l'on ait trois cycles entre l'unité d'émission et les ALU. Si une instruction est émise quand son opérande sort de l'ALU, elle prend plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''. Le nombre de cycles d'avance est égal au nombre d'étages entre l'unité de calcul et l'unité d'émission. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Et cela nous amène à la section suivante. Exploiter le contournement de l'unité mémoire demande de savoir combien de temps la lecture va prendre. Et le processeur n'a aucun moyen de le savoir. A la place, il spécule, comme il le fait avec la prédiction de branchement. Le processeur part du principe que l'accès mémoire fera un succès de cache, et émet les instructions lectures-dépendantes avec ce délai. Mais en cas de défaut de cache, les instructions lecture-dépendantes lisent des opérandes invalides. Et le processeur doit corriger le tir. Et pour cela, il a plusieurs solutions. Ce qui nous amène à la section suivante. ==Les défauts de cache sur un pipeline ''in-order''== Une instruction mémoire a un temps d'exécution variable, en raison des défauts de cache. En général, elle fait un succès de cache et accède au cache L1 de données. L'accès mémoire se fait en un cycle, parfois 2 cycles si le cache est pipeliné. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : une dizaine de cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation. Et si le défaut de cache se double d'un défaut de page mémoire, l'instruction peut dépasser le millier de cycles d'horloge. Les pipelines précédents doivent donc gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Un problème est que les accès mémoire ne peuvent pas être gérés comme les autres instructions multi-cycles, car ce sont des instructions au temps d'exécution variable et imprédictible. Et cela pose des problèmes sur plusieurs points. Premièrement, l'émission anticipée des micro-opérations est impactée, surtout pour contourner les lectures. Deuxièmement, cela pose des problèmes en matière d'ordre des écritures, pour les lectures et les écritures. ===Les défauts de cache en lecture causent des inversions d'écritures=== Lors d'un défaut de cache, il faut distinguer les lectures des écritures. Les écritures sont gérés par la file d'écriture, ce qui fait que si défaut de cache il y a, c'est une fois que l'écriture quitte la file d'écriture, quand l'instruction associée quitte le pipeline. Le défaut de cache peut alors être traité alors que le processeur continue d'exécuter des instructions dans son pipeline. Le processeur n'est pas bloqué, le système d'exception précise n'est pas impliqué. Par contre, pour les défauts de cache des lectures, les choses sont différentes. Les lectures lisent une donnée et l'écrivent dans un registre, son registre de destination. Elles écrivent donc dans les registres. Et les problèmes d'ordres des écritures peuvent survenir, en cas de défaut de cache. Si une lecture fait un défaut de cache, il faudra attendre plusieurs cycles avant qu'elle écrive dans le registre de destination. Mais les instructions suivant la lecture vont continuer à s’exécuter, voire écrire dans les registres. Un défaut de cache dure longtemps, une dizaine de cycles, voire plus. C'est souvent plus de cycles que la totalité du pipeline du processeur. Si les instructions après la lecture continuaient leur exécution, elles finiraient par atteindre la fin du pipeline. Le résultat est que ces instructions quitteraient le pipeline, écriraient dans les registres, voire dans la mémoire RAM, et ce serait définitif. Elles écriront dans les registres avant que la lecture le fasse, alors que c'est censé être l'inverse : il y a inversion de l'ordre des écritures. De plus, si la lecture déclenche une exception matérielle, on ne pourrait pas revenir en arrière, le système d'exception précise serait en erreur. Un autre défaut, lié au précédent, est que les instructions lecture-dépendantes sont émises en avance, en supposant que la lecture fait un défaut de cache. En cas de défaut de cache, elles liront un opérande invalide. Et ces instructions invalides doivent être annulées totalement. ===Le blocage du pipeline par l'unité mémoire=== Les processeurs ''in-order'' disposent de deux solutions pour corriger ces problèmes. La première utilise le système d'exceptions précises. Un défaut de cache est traité comme une exception matérielle un peu particulière. Lorsque l'unité mémoire détecte un défaut de cache, elle marque la donnée lue comme invalide et lève une exception matérielle "défaut de cache". Lorsque cette exception atteint l'étage de ''writeback'', celui-ci annule toutes les instructions qui suivent. Le processeur reprend son exécution à la lecture elle-même, éventuellement après un temps d'attente, pour tenir compte de la latence du cache L2, ou de la mémoire RAM. La seconde solution est de bloquer totalement le pipeline. Pour geler le pipeline, l'unité mémoire génère un signal ''pipeline stall''. Il est mis à 1 pendant qu'un défaut de cache est en cours, il est remis à zéro une fois le défaut de cache terminé. Ce signal est utilisé pour geler les registres du pipeline, dans les étages précédents. Seuls les étages situés avant l'étage MEM sont gelés. Ceux situés après peuvent faire leur travail normalement, ce qui permet au système d'exceptions précises de faire son travail. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé. [[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]] La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de cache est en cours. C'est seulement quand le défaut est terminé et qu'il reçoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline. ===Les optimisations : ne pas bloquer totalement le pipeline en cas de défaut de cache=== Bloquer le pipeline est une solution extrême. En théorie, il est possible de continuer à exécuter des instructions après un défaut de cache, sous certaines conditions. Il suffit juste que les instructions en question n'écrivent pas dans les registres. C'est même ce que font les processeurs à exécution dans le désordre. Ils ne sont pas bloqués après un défaut de cache. Cependant, ils peuvent le faire car ils ont un système d'exception précise élaboré, pas les processeurs ''in-order''. Néanmoins, cela suggère une optimisation. L'idée est de continuer à exécuter des instructions après le défaut de cache. L'unité mémoire est bloquée, elle ne peut pas accepter de nouvelle instruction, mais l'ALU entière, la FPU et les autres circuits sont utilisables. Les instructions émises après le défaut de cache sont alors appelées les ''instructions en avance''. Elles n'ont pas le droit d'enregistrer leurs résultats, que ce soit dans les registres ou la RAM. Pour cela, les micro-opérations restent dans le pipeline et sont stoppées dans le dernier étage libre. Par exemple, imaginons qu'un défaut de cache ait lieu à l'instant T, au cycle T. La lecture va se propager dans le pipeline jusqu'à l'étage de ''writeback'', dont à la toute fin du pipeline. La micro-opération suivante, émise au cycle T+2, s’arrêtera à l'étage d'avant. La micro-opération encore avant s’arrêtera à l'étage encore avant, et ainsi de suite. Le pipeline finira par se remplir totalement, et l'unité d'émission n'aura pas le choix que de bloquer le pipeline. Le résultat est que des micro-opérations auront été préchargées et exécutées à l'avance, mais ne se seront pas "terminées". Il y a toujours la possibilité de les annuler, en cas de problèmes. Le nombre de micro-opérations chargées en avance est égal au nombre d'étage du pipeline, vu qu'il est totalement remplit. Cette optimisation a possiblement été utilisée par les processeurs ARM de marque Cortex A53, A55 et A510, bien que ce ne soit pas certain (il s'agit d'une supposition fite par l'auteur du bloc "Chips and cheese". Le Cortex A52 et le A55 pouvaient exécuter en avance 8 instructions, et ils avaient un pipeline de 8 étages, alors que le A510 pouvait avoir 12 instructions en avance, et il avait justement un pipeline de 12 étages. {|class="wikitable" |- ! Fetch 1 !! Fetch 2 !! Décodage 1 !! Décodage 2 !! Émission !! Lecture des registres !! Exécution !! ''Writeback'' |- | Micro-opération T + 8 || Micro-opération T + 7 || Micro-opération T + 6 || Micro-opération T + 5 || Micro-opération T + 4 || Micro-opération T + 3 || Micro-opération T + 2 || Micro-opération T + 1 || colspan="8" class="f_rouge" | Lecture, en défaut au cycle T |} <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'émission des instructions | prevText=L'émission des instructions | next=Le scoreboard et le ROB | nextText=Le scoreboard et le ROB }} </noinclude> sjmmt2x5ahbnlfa732igk6qglnv3mgn 775086 775085 2026-10-12T20:21:57Z Mewtow 31375 /* Les défauts de cache en lecture causent des inversions d'écritures */ 775086 wikitext text/x-wiki Avec ce qu'on a dit dans le chapitre précédent, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipeline doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on va expliquer dans ce chapitre. Dans ce chapitre, nous allons voir les '''processeurs ''in-order'''''. Les définir n'est pas facile, surtout à ce stade du cours. Ils sont opposés aux processeurs avec exécution dans le désordre, qu'on verra dans quelques chapitres. La différence est ce qui se passe à la fin du pipeline. Un pipeline charge une nouvelle instruction par cycle, et celle-ci se propage dans le pipeline au rythme d'un étage par cycle d'horloge. On démarre les instructions une par une, dans un certain ordre. Mais le parcours du pipeline n'est pas un long fleuve tranquille, comme on le verra dans ce chapitre, et le résultat à la fin du pipeline est parfois différent. À la fin du pipeline, il se peut que des instructions arrivent dans le désordre. Une instruction a été bloquée pendant quelques cycles dans un étage, les autres ont continué de leur côté, les problèmes sur le chemin sont nombreux, comme on l'expliquera dans le reste du chapitre. Au final, deux instructions peuvent théoriquement arriver dans le désordre. Du moins, ce serait le cas si le processeur n'était pas conçu pour corriger le problème. Les processeurs ''in-order'' garantissent que des instructions chargées dans un certain ordre termineront dans le même ordre. Cela a de nombreux avantages, l'un d'entre eux est que cela permet au système exceptions précises, vu dans les chapitres précédents, de fonctionner au mieux. Les ''processeurs à exécution dans le désordre'', eux, acceptent que les instructions se terminent dans le désordre, pour des raisons de performance. Mais ils ont des systèmes pour éviter que cela pose problème. Ils utilisent pour cela des systèmes d'exception précises plus élaborés que ceux des chapitres précédents. Mais cela sera pour un autre chapitre. Pour ce chapitre, nous allons nous concentrer sur les processeurs ''in-order''. Mais avant toute chose, un petit rappel. Un processeur avec un pipeline est décomposé en deux parties. Le ''front end''), prend en charge les étages communs à toutes les instructions, comme le chargement et le décodage. L'amont est suivi une unité d'émission, elle-même suivie par le chemin de données. Le chemin de données contient plusieurs unités différentes : des unités de calcul, l'unité mémoire, parfois des unités pour les branchements. [[File:Unité d'émission.png|centre|vignette|upright=2|Unité d'émission]] Avec des instructions multi-cycles, le pipeline exploite plusieurs unités en même temps, c'est même la norme. Le processeur peut donc exécuter plusieurs instructions en parallèle, alors qu'elles ont été démarrées à des cycles différents. J'insiste sur ce point : le processeur émet une instruction par cycle, il en démarre une seule par cycle. Le processeur ne peut exécuter plusieurs instructions en même temps, que s'il a démarré une instruction multi-cycle. Le processeur exécute des opérations simples dans l'ALU entière, en parallèle de l'instruction multi-cycle. Néanmoins, cette possibilité fait face à de nombreuses contraintes. Et le reste du chapitre vise à expliquer quelles sont ces contraintes. ==L'inversion de l'ordre des écritures== Démarrer une nouvelle instruction par cycle est la base du pipeline. Mais avec des instructions multi-cycles, un problème bien particulier survient. Le processeur exécute plusieurs instructions en même temps, dans le même étage d’exécution, mais dans des ALUS/multiplieurs/FPU séparés. Le problème, c'est que si les instructions démarrent dans l'ordre du programme, les unes après les autres, elles peuvent terminer dans le désordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé ! Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre. On dit qu'il y a '''inversion de l'ordre des écritures'''. Et cette inversion cause de nombreux problèmes. Premier problème : des registres peuvent avoir une valeur invalide. Si les écritures changent d'ordre, une instruction peut lire un opérande qui n'est pas la bonne, qui n'a pas été écrite par la bonne instruction. Le programme ne s'exécute pas correctement. De plus, les mécanismes d'exception précises cessent de fonctionner, car ils supposent que les écritures dans les registres se font dans l'ordre. [[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]] Maintenant, autre problème, qui peut être vu comme un cas particulier du précédent. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Il y a un phénomène de '''double écriture simultanées'''. Et s'il s'agit là d'un exemple qui mélange dépendance structurelle et dépendance WAW. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! ... |- | Multiplication || Multiplication || Multiplication ||class="f_bleu" | Écriture || || |- | || Addition || Addition || class="f_bleu" | Ecriture || |} Il y a deux solutions principales pour régler ce problème, avec une troisième solution spécialement adaptée aux instructions mémoire, qui sont à part des autres. La première méthode permet d'utiliser des instructions multi-cycles, avec un pipeline fixe. La seconde utilise soit un pipeline fixe, soit un pipeline dynamique, mais avec des restrictions assez importantes. La gestion des défauts de cache est elle un cas à part, valable sur tous les types de pipelines. ===Les registres d'échelonnement=== La première solution est assez brute de décoffrage. Elle force toutes les instructions à prendre exactement le même nombre de cycles d'horloge. Elle utilise donc un pipeline où toutes les instructions ont le même nombre d'étages. Un tel pipeline est appelé un ''pipeline de longueur fixe'', ou encore un '''pipeline fixe'''. Et le processeur se cale sur les instructions les plus lentes. Par exemple, si une addition prend un cycle d'horloge, qu'une multiplication en prend 8 et qu'un décalage en prend 2, alors toutes les instructions prendront 8 cycles d'horloge. Et ces 8 cycles d'horloge correspondront à 8 étages d'exécution. [[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multi-cycles]] En conséquence, les instructions plus rapides auront donc des étages qui ne font rien. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire. Pour cela, on insère des registres entre la sortie de l'ALU et le banc de registre. Les registres en question sont appelés des ''staggering registers'', mais nous préférerons la traduction '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe. [[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]] Un défaut de la méthode précédente est que les données sont copiées d'un registre de pipeline au suivant à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs ''in order'' modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé. [[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]] ===Les blocages de pipeline=== Une solution alternative n'utilise ni registres d'échelonnage, ni étages qui ne font rien. Elle utilise à la place un '''pipeline dynamique''', de taille variable selon l'instruction. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. Les instructions multi-cycles prennent le temps qu'il faut pour faire leur calcul, les additions donnent leur résultat en un seul cycle, bref : les instructions ont des temps d'exécution variables. On ne se cale pas sur la plus lente. De plus, les instructions écrivent dans les registres au plus tôt, dès qu'elles ont fini leur calcul. [[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]] Pour éviter d'inverser les écritures, ce sont les instructions qui sont retardées. Pour comprendre comment ça marche, prenons l'exemple d'une multiplication suivie d'une addition, encore. La multiplication prend 8 cycles et occupe l'étage d'exécution pendant 8 cycles consécutifs. Pendant ces 8 cycles, l'instruction suivante attend dans l'unité de décodage, elle n'entre pas dans le chemin de donnée. Sa progression dans le pipeline est stoppée, et même que toutes les instructions précédentes. Elle ne redémarre que quand cela garantit que son écriture se fait après l'écriture de la multiplication. [[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]] Cette solution ''retarde les instructions'' pour qu'elles écrivent leurs résultats dans l'ordre. En clair, on retarde la seconde instruction histoire de remettre les écritures dans l'ordre. Lors de cette mise en attente, le pipeline est bloqué, mais seulement pour les étages précédant l'unité d'émission. Il s'agit donc d'un '''blocage sélectif''' du pipeline,n un blocage partiel. L'ordre de bloquer le pipeline est généré par l'unité d'émission. Dès qu'on doit bloquer le pipeline, c'est elle qui s'en charge, à une exception près sur laquelle nous reviendrons. L'unité d'émission envoie un signal ''pipeline stall'', qui gèle les registres de pipelines, pour les étages qui précédent l'unité d'émission. Le signal est maintenu tant que l'instruction multicycle s'exécute (à quelques cycles près). Reste à voir comment elle prend sa décision. [[File:Implémentation d'un blocage pipeline lors d'une opération multi-cycles.png|centre|vignette|upright=2.5|Implémentation d'un blocage pipeline lors d'une opération multi-cycles]] Une implémentation basique bloque le pipeline quand une instruction multi-cycles est en cours d'exécution. Pour cela, l'unité d'émission contient un compteur, qu'il initialise quand il démarre une micro-opération multi-cycles. Le compteur est initialisé avec le nombre de cycles d'horloge passés dans l'unité de calcul. Une nouvelle micro-opération peut démarrer si ce compteur est à zéro, le pipeline est bloqué sinon. Cependant, les performances sont grandement en-deças de ce qu'on pourrait espérer. En effet, la micro-opération candidate est émise avec un retard largement plus grand que nécessaire. Par exemple, imaginons la situation illustrée dans le tableau ci-dessous. Une multiplication, suivie d'une addition qui écrase ce résultat au cycle suivant. Cas complètement irréaliste, mais passons. L'addition prend deux cycles, la multiplication en prend 5. Sans pipeline, voici ce que ça donne : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || Addition || Addition || class="f_bleu" | Écriture || |} Maintenant, ajoutons un pipeline, et utilisons l'unité d'émission décrite plus haut, qui retarde la seconde instruction tant que la première n'est pas terminée. Avec cette implémentation basique, l'addition est retardée tant que la multiplication n'a pas écrit son résultat dans le registre. Les cycles de retard sont indiqués en rouge. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! ... |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Écriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Alors qu'en réalité, une solution optimale serait celle-ci : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Pour obtenir le cas optimal, le processeur utilise un '''''Result Shift Register'''''. Il s'agit d'un registre à décalage, dont le nombre de bits se cale sur l'instruction la plus longue. Par exemple, si le processeur exécute des additions en deux cycles, des décalages en trois cycles et des multiplications en 6 cycles, alors le ''result shift register'' aura 6 bits. Fait très important : on suppose que l'étage de ''writeback'' est comptée dans ces cycles. Et il en est de même pour les étages situés après ou avant l'étage d'exécution, du moment qu'ils suivent l'étage d'émission/le décodeur. Quand une instruction est émise, elle réserve le cycle d'horloge où elle compte faire une écriture. Les autres instructions ne pourront pas faire d'écriture lors d'un cycle réservé. Cela résout le problème des ''double écriture simultanées''. Pour réserver un cycle d'horloge, elle met le bit correspondant à 1, dans le ''result shift register''. Et on peut étendre la méthode pour gérer les dépendances WAW. Une instruction future ne doit pas faire d'écriture avant la micro-opération candidate. Pour cela, elle va empêcher les futures instructions de réserver les cycles précédents, ceux avant le cycle réservé. En clair : elle met les bits correspondant à 1. S'ils le sont déjà, ce n'est pas un problème : ils étaient à 1, ils restent à 1. les réservations faites par les instructions passées ne sont pas écrasées. Ainsi, une instruction qui s'exécute en N cycles réservera N étages de pipeline, N cycles consécutifs. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || 1 || 1 || 1 || 1 |} À chaque cycle d'horloge, ce registre est décalé d'un cran vers la droite. Cela permet de prendre en compte le fait que chaque instruction progresse d'un étage à chaque cycle d'horloge. Il va de soi que ce décalage est un décalage logique, qui remplit les vides par des zéros. Lorsque l'unité d'émission reçoit une instruction, elle vérifie le nombre de cycles qu'elle prend pour s’exécuter. Le nombre de cycles en question permet de savoir quel bit consulter dans le ''result shift''. Pour une instruction qui prend N cycles, elle vérifie si le bit numéro N de ce registre (en partant de la droite). S’il est à 1, cela signifie qu'une autre instruction a déjà réservé cet étage de pipeline. L'instruction candidate va devoir attendre que cet étage soit libre. Si ce bit est à 0, l’unité réserve alors le cycle associé en mettant le bit à 1, ainsi que tous les bits précédents, puis elle est émise. Par exemple, si je prends le cas du ''result shift register'' du tableau précédent. Imaginons qu'une instruction veuille s’exécuter pendant 4 cycles. L'unité d'émission regarde le quatrième bit. Elle voit qu'il est à 1, ce qui fait que ce n'est pas possible. Elle est alors retardée de un cycle. {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 1 || 1 || 1 || 1 |} Après le retard, le ''result shift register'' a été décalé, ce qui donne : {|class="wikitable" |+ Result Shift Register |- ! Cycle t+1 !! Cycle t+2 !! Cycle t+3 !! Cycle t+4 !! Cycle t+5 !! Cycle t+6 !! Cycle t+7 |- | 0 || 0 || 0 || class="f_rouge" | 0 || 1 || 1 || 1 |} Le bit associé vaut 0, ce qui fait que l'instruction peut être émise. Elle met alors ce bit à 1, ainsi que tous les précédents (ils le sont déjà). ===La solution des processeurs MIPS=== Une solution alternative a été utilisée sur les premiers processeurs MIPS. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications. Les multiplications étaient les seules instructions multi-cycles sur ces processeurs. Les instructions multi-cycles disposaient de leur propre registres rien que pour elles. Précisément, les instructions de multiplication enregistraient leur résultat dans des registres LO et HI, pas dans les registres généraux. Le fait de séparer les registres résout ce problème d'inversion des écritures. L'inversion des écritures survient quand on mixe des instructions avec des temps d'exécution différents, mais qui écrivent dans les mêmes registres. Ici, on sépare les registres et les instructions suivant leurs temps d'exécution. Les multiplications étaient émises dans l'ordre, avaient toute le même temps d'exécution : les résultats arrivaient forcément en ordre dans les registres HI/LO. Idem pour les autres instructions qui prenaient un seul cycle : elles étaient émises dans l'ordre, prenaient toutes un cycle, et enregistraient leurs résultats dans l'ordre dans les registres généraux. ==Les dépendances d'instruction== Les problèmes liés aux instructions multi-cycles sont des cas particuliers d'un problème plus général, que tous les pipelines ont. Un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée. Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, les instructions ont des dépendances très variées. Les dépendances qui vont nous intéresser sont les '''dépendances de données'''. Nous avons déjà parlé des '''dépendances structurelles''' au chapitre précédent, aussi je ne reviendrais pas dessus. Il y a aussi les '''dépendances de contrôle''' sont le fait des branchements, interruptions et exceptions, qui fait l'objet des chapitres sur la prédiction de branchement, et d'une section sur les exceptions précises. ===Les dépendances de données : registres et adresses=== Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c’est-à-dire au même registre ou la même adresse mémoire. Et cela nous amène à distinguer deux types de dépendances : de registre, d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. Elles surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Et cela implique d'avoir deux instructions mémoire en vol, ce qui implique que le pipeline accepte plusieurs accès mémoire "simultanés". Et cette possibilité n'est pas réservée aux pipelines complexes. Nous verrons d'ailleurs un exemple frappant dans la fin de ce chapitre. Mais il faut avouer que les dépendances d'adresse ne posent problème que sur les processeurs dotés d'exécution dans le désordre, qui seront vus dans plusieurs chapitres. Aussi, pour le moment, concentrons-nous sur les dépendances de registres. Les dépendances de registre surviennent quand deux instructions lisent ou écrivent un registre. Deux instructions, deux types d'accès (lecture ou écriture), cela fait quatre possibilités : * '''Read after read''' : Deux instructions lisent le même registre, mais pas en même temps. * '''Read after write''' : Une instruction lit le résultat écrit par l'autre. * '''Write after write''' : Deux instructions écrivent dans le même registre, mais pas en même temps. * '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction. Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances sont bloquantes. Dans le pire des cas, elles imposent que la première instruction termine son exécution, avant que la seconde soit démarrée. Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. Les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. Concrètement, elles n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW existent à condition que le pipeline supporte les écritures dans le désordre. Or, nous avons vu plus haut comment remettre les écritures dans l'ordre. Les pipelines que nous avons abordés ne sont pas concernés par les dépendances WAW. Aussi, nous repoussons leur étude aux processeurs avec exécution dans le désordre. ===L'unité d’émission : la détection des dépendances de registres=== Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions, ce qui peut être fait après l'unité de décodage. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites, qui sont souvent calculées/déterminées à l'exécution. Les deux types de dépendances sont donc le fait de circuits séparés, dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité mémoire, les autres dépendances sont gérées dans une '''unité d'émission''', séparée du décodeur d'instruction, qui a son propre étage de pipeline. L'unité d'émission détecte les dépendances de registres et met en attente les micro-opérations si besoin. Pour simplifier, c'est elle bloque le pipeline quand c'est nécessaire. Et mine de rien, la section sur les instructions multi-cycle nous a montré son rôle. C'est elle qui gèle le pipeline lors d'une instruction multi-cycle. Par contre, elle ne gère pas les défauts de cache, qui sont gérés par l'unité mémoire. {|class="wikitable" |+ Pipeline basique, avec une unité d'émission |- ! Calcul du ''Program Counter'' !! Chargement !! Décodage !! class="f_rouge" | Émission !! Lecture des opérandes (registres) !! Exécution (ALU) !! ''Writeback'' |- | rowspan="2" | ''Program Counter'' | rowspan="2" | Cache d'instruction | rowspan="2" | Décodeur d'instruction | rowspan="2" class="f_rouge" | Unité d'émission | rowspan="2" | Port de lecture du banc de registre (registres) | Unités de calcul | rowspan="2" | Port d'écriture du banc de registre |- | class="f_rouge" | Unité mémoire |} À chaque cycle, l'unité d'émission reçoit une micro-opération et analyse ses dépendances de données. Si elle a une dépendance bloquante, elle est mise en attente dans l'étage d'émission, en attendant que les conditions soient réunies pour pouvoir s'exécuter. Durant ce temps d'attente, le pipeline est bloqué. Sinon, l'unité d'émission la laisse passer à l'étage suivant. Commençons par étudier les dépendances RAW, où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction qui produit le résultat est appelée l''''instruction productrice''', alors que celle qui l'utilise est appelée l''''instruction consommatrice'''. Les deux instructions ont alors une dépendance de donnée, dite de type '''''Read After Write'''''. Et cette dépendance impose que l'instruction productrice soit totalement terminée avant qu'on émette l'instruction consommatrice. La raison est que le résultat de l'instruction productrice doit être enregistré dans les registres, avant d'être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Du fait de ce délai, il se peut que l'instruction consommatrice lise le registre avant que le résultat soit enregistré. Si l'instruction consommatrice est lancée immédiatement après l'instruction productrice, ça arrivera avec certitude. Pour éviter cela, il suffit de retarder l'exécution de l'instruction consommatrice tant que le résultat de l'instruction productrice n'est pas disponible, pas encore dans les registres. Et il se peut qu'elle soit retardée durant quelques cycles pour cela. Et ce retard implique, vous l'aurez deviné, de bloquer le pipeline. Précision importante : les dépendances RAW ne sont pertinentes que pour les micro-opérations en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol. Mais comment le processeur détecte les dépendances de registres entre deux instructions ? La réponse est simple : il compare les registres utilisés par les deux instructions. Les registres étant connus dès la sortie de l'étape de décodage, l'unité de décodage peut les comparer. L'unité d'émission reçoit une micro-opération décodée, appelée '''micro-opération candidate'''. Elle détecte si cette micro-opération candidate a une dépendance avec les micro-opérations qui sont dans le pipeline, dans les étages suivant l'unité d'émission. Les micro-opérations en question seront appelées '''micro-opérations "en vol"'''. L'unité d'émission compare les registres utilisés par la micro-opération candidate et les micro-opérations en vol. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Une dépendance RAW a lieu entre une instruction consommatrice et une instruction productrice. L'instruction productrice écrit son résultat dans un registre, son registre de destination. L'instruction consommatrice utilise ce résultat comme opérande, ce qui fait que le résultat est dans un de ses registres opérande. Une dépendance RAW implique que l'instruction productrice est en vol, alors que l'instruction consommatrice est candidate. L'unité d'émission doit donc comparer les registres opérandes de l'instruction candidate, avec les registres de destination des instructions en vol. L'unité d'émission doit donc connaitre les registres de destination des micro-opérations en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons. [[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]] Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être lu par une autre instruction. Les registres qui ne sont pas réservés sont dits libres ou 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é, ce qui implique que c'est le registre de destination d'une instruction en vol. L'instruction n'est pas émise et le pipeline est bloqué. Le registre encode les registres réservés en représentation ''one-hot'' modifiée. 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. Un registre est réservé en mettant le bit associé à 1. {|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 |} Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''. 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 l'instruction, 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 l'instruction 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. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. 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 l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul. Diverses optimisations permettent de mitiger l'impact des dépendances RAW. Et dans la section suivante, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. ==Le contournement : réduire les dépendances== Nous venons de voir que les dépendances imposent de bloquer le pipeline, pendant quelques cycles. Et encore une fois, c'est l'unité d'émission qui s'en charge. Du moins, c'est le cas sans optimisation. Une optimisation permet de fortement réduire les blocages de pipelines liés aux dépendances RAW, jusqu'à les annuler dans certains cas. L'optimisation en question est celle du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] Le contournement fait que deux instructions consécutives avec une dépendance RAW peuvent quand même être émises. Mais comme on va le voir, cela n'élimine pas les blocages de pipelines liés aux dépendances RAW. L'unité d'émission existe toujours, elle n'est pas forcément simplifiée, au contraire. Par contre, les blocages de pipeline sont moins fréquents, et les performances augmentent. : Avant de poursuivre, rappelons le vocabulaire. Si une instruction produit un résultat, qui utilisé par une seconde instruction comme opérande : la première instruction est appelée l''''instruction productrice''', la seconde est appelée l''''instruction consommatrice'''. ===Le contournement de l'unité de calcul=== La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée. [[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]] Le résultat d'une opération est mémorisé en sortie de l'ALU, et précisément dans le registre de pipeline, au cycle suivant. Le contournement connecte ce registre de pipeline sur l'entrée de l'ALU si besoin, et on le déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs. [[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]] Intuitivement, on se dit que c'est l'unité d'émission qui commande le multiplexeur. Mais il est possible de déporter cette fonction en-dehors de l'unité d'émission, de gérer cela au niveau "local". La commande du multiplexeur est réalisée par un circuit situé pas loin de l'ALU elle-même. L'unité d'émission émet juste deux micro-opérations consécutives qui ont une dépendance RAW, à condition que le résultat de l'une soit immédiatement consommé par l'autre, et que les deux utilisent l'ALU entière. Pour commander les multiplexeurs, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul. [[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]] La présence de plusieurs unités de calcul complexifie le contournement. Un processeur a typiquement une ALU entière, un ''barrel shifter'' et un multiplieur entier. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque circuit à la fois sur son entrée, mais aussi sur l'entrée des deux autres. En tout, cela demande 9 interconnexions, avec autant de multiplexeurs. Et c'est la même chose à l'intérieur d'une FPU. Pour rappel une FPU est typiquement composée de deux circuits : un additionneur flottant et un multiplieur flottant. Non seulement ils ont leur sortie connectée à leur entrée, mais en plus, la sortie de l'un est relié à l'entrée de l'autre. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand-chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies entre registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un entre FPUs. [[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]] Il faut aussi penser au cas où les ALU entières calculent des adresses mémoire. Dans ce cas, il faut aussi connecter une unité de calcul à l'unité mémoire. Le ''barrel shifter'' et le multiplieur ne sont pas concernés, seule l'ALU entière capable de faire des additions l'est. La connexion est unidirectionnelle, de l'ALU entière vers l'unité mémoire. Si l'unité mémoire incorpore sa propre unité de calcul, il faut faire de même, mais pas pour les mêmes raisons. Le réseau de contournement ne transmet alors pas des adresses mémoire calculées, mais les opérandes de ces calculs. Ces opérandes viennent soit des registres, soit sont calculées dans l'ALU entière. Mais le réseau de contournement est alors plus complexe, car toutes les ALU entières peuvent calculer ces opérandes. En pratique, c'est rarement fait. L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires. ===Le contournement des lectures mémoire=== La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand une lecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué). Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre. Et cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire. [[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]] La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs. [[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]] Ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt. [[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]] La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux. [[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]] La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour. ===Le contournement des instructions multi-cycles=== La durée des instructions est à prendre en compte pour effectuer un contournement convenable. Le contournement entre instructions simples est facile, vu qu'elles s'exécutent en un seul cycle. Leur résultat peut être envoyé immédiatement sur leur entrée. Il n'y a aucun besoin de bloquer le pipeline. Mais avec les instructions multi-cycles, les choses sont plus compliquées, et cela mérite un exemple explicatif. Un exemple est celui où une multiplication de 5 cycles est suivie par une addition de deux cycles. Si les deux instructions sont indépendantes, la seconde sera retardée, en raison des dépendances WAW, mais pas à cause des dépendances RAW. Voici comment sont exécutées les deux instructions : {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} Maintenant, supposons que l'addition a pour opérande le résultat de la multiplication. On voit que l'on ne peut pas reprendre tel quel le tableau précédent, car l'addition serait émise trop tôt. Il faut donc retarder encore l'addition, pour qu'elle démarre quand la multiplication vient tout juste de terminer. {|class="wikitable" |- ! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 |- | Multiplication || Multiplication || Multiplication || Multiplication || Multiplication || class="f_bleu" | Ecriture || || || |- | || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || class="f_rouge" | Retard || Addition || Addition || class="f_bleu" | Ecriture || |} L'unité d'émission retarde l'émission d'une instruction pendant quelques cycles, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas. Le contournement doit aussi tenir compte des registres d'échelonnage. Il arrive en effet qu'une instruction soit émise alors que son opérande est encore dans les registres d'échelonnage. Ce n'est pas rare, c'est même assez fréquent. Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Mais lire les opérandes dans les registres d'échelonnage nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin. ===L’émission anticipée des micro-opérations=== Idéalement, on voudrait démarrer une nouvelle micro-opération sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU. Et cela peut poser problème pour le contournement. Imaginons que l'on ait trois cycles entre l'unité d'émission et les ALU. Si une instruction est émise quand son opérande sort de l'ALU, elle prend plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''. Le nombre de cycles d'avance est égal au nombre d'étages entre l'unité de calcul et l'unité d'émission. L'émission anticipée demande de connaitre à l'avance quand une instruction productrice fournira son résultat. Concrètement, c'est possible pour une instruction de calcul, mais pas pour un accès mémoire, dont le temps d'exécution est variable. Si on veut faire du contournement pour les lectures, ça ne peut être que spéculatif. Et cela nous amène à la section suivante. Exploiter le contournement de l'unité mémoire demande de savoir combien de temps la lecture va prendre. Et le processeur n'a aucun moyen de le savoir. A la place, il spécule, comme il le fait avec la prédiction de branchement. Le processeur part du principe que l'accès mémoire fera un succès de cache, et émet les instructions lectures-dépendantes avec ce délai. Mais en cas de défaut de cache, les instructions lecture-dépendantes lisent des opérandes invalides. Et le processeur doit corriger le tir. Et pour cela, il a plusieurs solutions. Ce qui nous amène à la section suivante. ==Les défauts de cache sur un pipeline ''in-order''== Une instruction mémoire a un temps d'exécution variable, en raison des défauts de cache. En général, elle fait un succès de cache et accède au cache L1 de données. L'accès mémoire se fait en un cycle, parfois 2 cycles si le cache est pipeliné. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : une dizaine de cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation. Et si le défaut de cache se double d'un défaut de page mémoire, l'instruction peut dépasser le millier de cycles d'horloge. Les pipelines précédents doivent donc gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Un problème est que les accès mémoire ne peuvent pas être gérés comme les autres instructions multi-cycles, car ce sont des instructions au temps d'exécution variable et imprédictible. Et cela pose des problèmes sur plusieurs points. Premièrement, l'émission anticipée des micro-opérations est impactée, surtout pour contourner les lectures. Deuxièmement, cela pose des problèmes en matière d'ordre des écritures, pour les lectures et les écritures. ===Les défauts de cache en lecture causent des inversions d'écritures=== Lors d'un défaut de cache, il faut distinguer les lectures des écritures. Les écritures sont gérés par la file d'écriture, ce qui fait que si défaut de cache il y a, c'est une fois que l'écriture quitte la file d'écriture, quand l'instruction associée quitte le pipeline. Le défaut de cache peut alors être traité alors que le processeur continue d'exécuter des instructions dans son pipeline. Le processeur n'est pas bloqué, le système d'exception précise n'est pas impliqué. Par contre, pour les défauts de cache des lectures, les choses sont différentes. Les lectures lisent une donnée et l'écrivent dans un registre, son registre de destination. Elles écrivent donc dans les registres. Et les problèmes d'ordres des écritures peuvent survenir, en cas de défaut de cache. Si une lecture fait un défaut de cache, il faudra attendre plusieurs cycles avant qu'elle écrive dans le registre de destination. Mais les instructions suivant la lecture vont continuer à s’exécuter, voire écrire dans les registres. Un défaut de cache dure longtemps, une dizaine de cycles, voire plus. C'est souvent plus de cycles que la totalité du pipeline du processeur. Si les instructions après la lecture continuaient leur exécution, elles finiraient par atteindre la fin du pipeline. Le résultat est que ces instructions quitteraient le pipeline, écriraient dans les registres, voire dans la mémoire RAM, et ce serait définitif. Elles écriront dans les registres avant que la lecture le fasse, alors que c'est censé être l'inverse : il y a inversion de l'ordre des écritures. De plus, si la lecture déclenche une exception matérielle, on ne pourrait pas revenir en arrière, le système d'exception précise serait en erreur. Un autre défaut, lié au précédent, est que les instructions lecture-dépendantes sont émises en avance, en supposant que la lecture fait un défaut de cache. En cas de défaut de cache, elles liront un opérande invalide. Et ces instructions invalides doivent être annulées totalement. En clair, les défauts de cache en lecture causent de nombreux problèmes : inversion de l'ordre des écritures, problèmes avec l'émission anticipée qui causent des violations de dépendances RAW. Autant dire qu'il faut corriger ce problème. ===Le blocage du pipeline par l'unité mémoire=== Les processeurs ''in-order'' disposent de deux solutions pour corriger ces problèmes. La première utilise le système d'exceptions précises. Un défaut de cache est traité comme une exception matérielle un peu particulière. Lorsque l'unité mémoire détecte un défaut de cache, elle marque la donnée lue comme invalide et lève une exception matérielle "défaut de cache". Lorsque cette exception atteint l'étage de ''writeback'', celui-ci annule toutes les instructions qui suivent. Le processeur reprend son exécution à la lecture elle-même, éventuellement après un temps d'attente, pour tenir compte de la latence du cache L2, ou de la mémoire RAM. La seconde solution est de bloquer totalement le pipeline. Pour geler le pipeline, l'unité mémoire génère un signal ''pipeline stall''. Il est mis à 1 pendant qu'un défaut de cache est en cours, il est remis à zéro une fois le défaut de cache terminé. Ce signal est utilisé pour geler les registres du pipeline, dans les étages précédents. Seuls les étages situés avant l'étage MEM sont gelés. Ceux situés après peuvent faire leur travail normalement, ce qui permet au système d'exceptions précises de faire son travail. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé. [[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]] La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de cache est en cours. C'est seulement quand le défaut est terminé et qu'il reçoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline. ===Les optimisations : ne pas bloquer totalement le pipeline en cas de défaut de cache=== Bloquer le pipeline est une solution extrême. En théorie, il est possible de continuer à exécuter des instructions après un défaut de cache, sous certaines conditions. Il suffit juste que les instructions en question n'écrivent pas dans les registres. C'est même ce que font les processeurs à exécution dans le désordre. Ils ne sont pas bloqués après un défaut de cache. Cependant, ils peuvent le faire car ils ont un système d'exception précise élaboré, pas les processeurs ''in-order''. Néanmoins, cela suggère une optimisation. L'idée est de continuer à exécuter des instructions après le défaut de cache. L'unité mémoire est bloquée, elle ne peut pas accepter de nouvelle instruction, mais l'ALU entière, la FPU et les autres circuits sont utilisables. Les instructions émises après le défaut de cache sont alors appelées les ''instructions en avance''. Elles n'ont pas le droit d'enregistrer leurs résultats, que ce soit dans les registres ou la RAM. Pour cela, les micro-opérations restent dans le pipeline et sont stoppées dans le dernier étage libre. Par exemple, imaginons qu'un défaut de cache ait lieu à l'instant T, au cycle T. La lecture va se propager dans le pipeline jusqu'à l'étage de ''writeback'', dont à la toute fin du pipeline. La micro-opération suivante, émise au cycle T+2, s’arrêtera à l'étage d'avant. La micro-opération encore avant s’arrêtera à l'étage encore avant, et ainsi de suite. Le pipeline finira par se remplir totalement, et l'unité d'émission n'aura pas le choix que de bloquer le pipeline. Le résultat est que des micro-opérations auront été préchargées et exécutées à l'avance, mais ne se seront pas "terminées". Il y a toujours la possibilité de les annuler, en cas de problèmes. Le nombre de micro-opérations chargées en avance est égal au nombre d'étage du pipeline, vu qu'il est totalement remplit. Cette optimisation a possiblement été utilisée par les processeurs ARM de marque Cortex A53, A55 et A510, bien que ce ne soit pas certain (il s'agit d'une supposition fite par l'auteur du bloc "Chips and cheese". Le Cortex A52 et le A55 pouvaient exécuter en avance 8 instructions, et ils avaient un pipeline de 8 étages, alors que le A510 pouvait avoir 12 instructions en avance, et il avait justement un pipeline de 12 étages. {|class="wikitable" |- ! Fetch 1 !! Fetch 2 !! Décodage 1 !! Décodage 2 !! Émission !! Lecture des registres !! Exécution !! ''Writeback'' |- | Micro-opération T + 8 || Micro-opération T + 7 || Micro-opération T + 6 || Micro-opération T + 5 || Micro-opération T + 4 || Micro-opération T + 3 || Micro-opération T + 2 || Micro-opération T + 1 || colspan="8" class="f_rouge" | Lecture, en défaut au cycle T |} <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'émission des instructions | prevText=L'émission des instructions | next=Le scoreboard et le ROB | nextText=Le scoreboard et le ROB }} </noinclude> oyzjn6qw3nz5403e6gpfewi9uacg0tw Mathc initiation/007q 0 84573 775034 774809 2026-10-12T12:50:38Z Xhungab 23827 775034 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/007n| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c2fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c2fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f2.h" /* ---------------------------------- */ int main(void) { double T = 2*PI; double omega = (2.*PI/T); double a = -PI; double b = +PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> (t^2)) Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +3.289868134 n an bn Amplitude Phase 1 |-4.000e+00 | +0.000e+00 | +4.000e+00 | +3.142e+00 rad = +180.00° 2 |+1.000e+00 | +0.000e+00 | +1.000e+00 | +0.000e+00 rad = +0.00° 3 |-4.444e-01 | +0.000e+00 | +4.444e-01 | +3.142e+00 rad = +180.00° 4 |+2.500e-01 | +0.000e+00 | +2.500e-01 | +0.000e+00 rad = +0.00° 5 |-1.600e-01 | +0.000e+00 | +1.600e-01 | +3.142e+00 rad = +180.00° 6 |+1.111e-01 | +0.000e+00 | +1.111e-01 | +0.000e+00 rad = +0.00° 7 |-8.163e-02 | +0.000e+00 | +8.163e-02 | +3.142e+00 rad = +180.00° 8 |+6.250e-02 | +0.000e+00 | +6.250e-02 | +0.000e+00 rad = +0.00° 9 |-4.938e-02 | +0.000e+00 | +4.938e-02 | +3.142e+00 rad = +180.00° Press return to continue. </syntaxhighlight> {{AutoCat}} 5204wyuhda0wfpwec0i75v5ksh4zg4p 775045 775034 2026-10-12T13:13:03Z Xhungab 23827 775045 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/007n| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c2fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c2fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f2.h" /* ---------------------------------- */ int main(void) { double T = 2*PI; double omega = (2.*PI/T); double a = -PI; double b = +PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s t = [-PI..PI]\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> (t^2)) t = [-PI..PI] Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +3.289868134 n an bn Amplitude Phase 1 |-4.000e+00 | +0.000e+00 | +4.000e+00 | +3.142e+00 rad = +180.00° 2 |+1.000e+00 | +0.000e+00 | +1.000e+00 | +0.000e+00 rad = +0.00° 3 |-4.444e-01 | +0.000e+00 | +4.444e-01 | +3.142e+00 rad = +180.00° 4 |+2.500e-01 | +0.000e+00 | +2.500e-01 | +0.000e+00 rad = +0.00° 5 |-1.600e-01 | +0.000e+00 | +1.600e-01 | +3.142e+00 rad = +180.00° 6 |+1.111e-01 | +0.000e+00 | +1.111e-01 | +0.000e+00 rad = +0.00° 7 |-8.163e-02 | +0.000e+00 | +8.163e-02 | +3.142e+00 rad = +180.00° 8 |+6.250e-02 | +0.000e+00 | +6.250e-02 | +0.000e+00 rad = +0.00° 9 |-4.938e-02 | +0.000e+00 | +4.938e-02 | +3.142e+00 rad = +180.00° Press return to continue. </syntaxhighlight> {{AutoCat}} hx48f9ibkmrmorqjk0rqqgezn4157ye Mathc initiation/a9c 0 84574 775035 774810 2026-10-12T12:53:38Z Xhungab 23827 775035 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a602| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c1fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c1fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f1.h" /* ---------------------------------- */ int main(void) { double T = 2.; double omega = (2.*PI/T); double a = -1; double b = +1; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> (x(x+1)) Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +0.333333333 n an bn Amplitude Phase 1 |-4.053e-01 | +6.366e-01 | +7.547e-01 | +2.138e+00 rad = +122.48° 2 |+1.013e-01 | -3.183e-01 | +3.340e-01 | -1.263e+00 rad = -72.34° 3 |-4.503e-02 | +2.122e-01 | +2.169e-01 | +1.780e+00 rad = +101.98° 4 |+2.533e-02 | -1.592e-01 | +1.612e-01 | -1.413e+00 rad = -80.96° 5 |-1.621e-02 | +1.273e-01 | +1.284e-01 | +1.697e+00 rad = +97.26° 6 |+1.126e-02 | -1.061e-01 | +1.067e-01 | -1.465e+00 rad = -83.94° 7 |-8.271e-03 | +9.095e-02 | +9.132e-02 | +1.661e+00 rad = +95.20° 8 |+6.332e-03 | -7.958e-02 | +7.983e-02 | -1.491e+00 rad = -85.45° 9 |-5.003e-03 | +7.074e-02 | +7.091e-02 | +1.641e+00 rad = +94.05° Press return to continue. </syntaxhighlight> {{AutoCat}} qvx72szy5am399sffjtcbd7uquasddt 775046 775035 2026-10-12T13:14:00Z Xhungab 23827 775046 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a602| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c1fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c1fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f1.h" /* ---------------------------------- */ int main(void) { double T = 2.; double omega = (2.*PI/T); double a = -1; double b = +1; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s t = [-1..1]\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="c"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> (x(x+1)) t = [-1..1] Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +0.333333333 n an bn Amplitude Phase 1 |-4.053e-01 | +6.366e-01 | +7.547e-01 | +2.138e+00 rad = +122.48° 2 |+1.013e-01 | -3.183e-01 | +3.340e-01 | -1.263e+00 rad = -72.34° 3 |-4.503e-02 | +2.122e-01 | +2.169e-01 | +1.780e+00 rad = +101.98° 4 |+2.533e-02 | -1.592e-01 | +1.612e-01 | -1.413e+00 rad = -80.96° 5 |-1.621e-02 | +1.273e-01 | +1.284e-01 | +1.697e+00 rad = +97.26° 6 |+1.126e-02 | -1.061e-01 | +1.067e-01 | -1.465e+00 rad = -83.94° 7 |-8.271e-03 | +9.095e-02 | +9.132e-02 | +1.661e+00 rad = +95.20° 8 |+6.332e-03 | -7.958e-02 | +7.983e-02 | -1.491e+00 rad = -85.45° 9 |-5.003e-03 | +7.074e-02 | +7.091e-02 | +1.641e+00 rad = +94.05° Press return to continue. </syntaxhighlight> {{AutoCat}} 3i8467ct23qwb60ym0k531jjht6qxry Mathc initiation/a9d 0 84575 775033 774811 2026-10-12T12:45:23Z Xhungab 23827 775033 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a593| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c0fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c0fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = PI; double omega = (2.*PI/T); double a = 0.; double b = PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+0.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +1.570796327 n an bn Amplitude Phase 1 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 2 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 4 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | -2.000e-01 | +2.000e-01 | -1.571e+00 rad = -90.00° 6 |+0.000e+00 | -1.667e-01 | +1.667e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | -1.429e-01 | +1.429e-01 | -1.571e+00 rad = -90.00° 8 |+0.000e+00 | -1.250e-01 | +1.250e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | -1.111e-01 | +1.111e-01 | -1.571e+00 rad = -90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} bnzj86rv74tgoh609166c33e0kwkulo 775036 775033 2026-10-12T12:54:34Z Xhungab 23827 775036 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a593| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c0fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c0fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = PI; double omega = (2.*PI/T); double a = 0.; double b = PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +1.570796327 n an bn Amplitude Phase 1 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 2 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 4 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | -2.000e-01 | +2.000e-01 | -1.571e+00 rad = -90.00° 6 |+0.000e+00 | -1.667e-01 | +1.667e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | -1.429e-01 | +1.429e-01 | -1.571e+00 rad = -90.00° 8 |+0.000e+00 | -1.250e-01 | +1.250e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | -1.111e-01 | +1.111e-01 | -1.571e+00 rad = -90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} 3d9nslpskcuyx6xputjnlybyr6gqpr7 775043 775036 2026-10-12T13:10:36Z Xhungab 23827 775043 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a593| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c0fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c0fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = PI; double omega = (2.*PI/T); double a = 0.; double b = PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s t = [0..PI]\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t t = [0..PI] Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +1.570796327 n an bn Amplitude Phase 1 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 2 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 4 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | -2.000e-01 | +2.000e-01 | -1.571e+00 rad = -90.00° 6 |+0.000e+00 | -1.667e-01 | +1.667e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | -1.429e-01 | +1.429e-01 | -1.571e+00 rad = -90.00° 8 |+0.000e+00 | -1.250e-01 | +1.250e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | -1.111e-01 | +1.111e-01 | -1.571e+00 rad = -90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} icmf3zzfdeyluzijq7m1a2rpcq8o4wi Mathc initiation/007y 0 84577 775039 774908 2026-10-12T13:00:34Z Xhungab 23827 775039 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/a594#Se familiariser avec les series de Fourier :| Sommaire]] : : ==La fonction : f(t) = t ...................... avec t = [-PI..PI]== : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]] : . : ==La fonction f :== * [[Mathc initiation/a595|f.h ]] ...... '''f(t) = t avec t = [-PI..PI]''' : . : ==Les coefficients de la séries de Fourier :== * [[Mathc initiation/007s|c3a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0=\frac1T\int_Tf(t)\,\mathrm{d}t}</math> * [[Mathc initiation/007t|c3an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> * [[Mathc initiation/007u|c3bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math> . {| class="wikitable" |+ Séries de Fourier |- ! plot [0.:pi] [0.:pi] !! plot [-pi:4*pi] [-3.5:4.0] |- | [[File:Fourier serie for my wikibook.png|thumb|]] || [[File:Fourier serie for my wikibook 5.png|thumb]]] |} Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007z|gnuplot]]''' '''les 9 premiers termes''' de la série. * [[Mathc initiation/007v|c3fs.c]] Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série. * [[Mathc initiation/007w|c3fsN.c]] ==Calculons les amplitudes An et les phases φn== * [[Mathc initiation/007x|c3fsap.c]] {{AutoCat}} ql4k51d7h80e4qmnleqhdds8jxjeksi Mathc initiation/007x 0 84583 775032 774898 2026-10-12T12:39:00Z Xhungab 23827 775032 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/007y| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier| c3fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c3fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = 2.*PI; double omega = (2.*PI/T); double a = -PI; double b = +PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+0.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +0.000000000 n an bn Amplitude Phase 1 |+0.000e+00 | +2.000e+00 | +2.000e+00 | +1.571e+00 rad = +90.00° 2 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | +6.667e-01 | +6.667e-01 | +1.571e+00 rad = +90.00° 4 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | +4.000e-01 | +4.000e-01 | +1.571e+00 rad = +90.00° 6 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | +2.857e-01 | +2.857e-01 | +1.571e+00 rad = +90.00° 8 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | +2.222e-01 | +2.222e-01 | +1.571e+00 rad = +90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} 71f5vpxce8c657aj6rm8jgadprb08mo 775037 775032 2026-10-12T12:55:10Z Xhungab 23827 775037 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/007y| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier| c3fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c3fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = 2.*PI; double omega = (2.*PI/T); double a = -PI; double b = +PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +0.000000000 n an bn Amplitude Phase 1 |+0.000e+00 | +2.000e+00 | +2.000e+00 | +1.571e+00 rad = +90.00° 2 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | +6.667e-01 | +6.667e-01 | +1.571e+00 rad = +90.00° 4 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | +4.000e-01 | +4.000e-01 | +1.571e+00 rad = +90.00° 6 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | +2.857e-01 | +2.857e-01 | +1.571e+00 rad = +90.00° 8 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | +2.222e-01 | +2.222e-01 | +1.571e+00 rad = +90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} t43ppe7gqv0hiszhp0zu5cmwhhcf1oi 775044 775037 2026-10-12T13:11:37Z Xhungab 23827 775044 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/007y| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier| c3fsap.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c3fsap.c */ /* ---------------------------------- */ #include "x_hfile.h" /* ---------------------------------- */ #define N 10 /* ---------------------------------- */ #include "f0.h" /* ---------------------------------- */ int main(void) { double T = 2.*PI; double omega = (2.*PI/T); double a = -PI; double b = +PI; double A[N+1]; double B[N+1]; int n = 0; A[0] = 1./T * fourier_a0(f, a,b); for( n=1; n<=N; n++) { A[n] = 2./T * fourier_an(f, a,b, omega, n); B[n] = 2./T * fourier_bn(f, a,b, omega, n); if (fabs(A[n])<(+1.000e-10)) A[n] = 0.; if (fabs(B[n])<(+1.000e-10)) B[n] = 0.; } clrscrn(); printf(" Fourier Series\n\n" " _+oo_ \n" " \\ \n" " S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n" " /_____ \n" " n = 1 \n\n\n" " (a + T \n" " a0 = 1/T int( f(t) dx\n" " (a \n\n\n" " (a + T \n" " an = 2/T int( f(t) cos(n omega t) dt\n" " (a \n\n" " (a + T \n" " bn = 2/T int( f(t) sin(n omega t) dt\n" " (a \n\n"); stop(); clrscrn(); printf(" With f : t-> %s t = [-PI..PI]\n\n" " Amplitude spectrum : sqrt(an^2 + bn^2) \n" " Phase spectrum : atan2(bn,an) \n\n",feq); printf(" A[0] = a0 = %+.9f\n\n", A[0]); printf(" n an bn Amplitude Phase\n\n"); for( n=1; n<N; n++) printf("%2d |%+7.3e | %+7.3e | %+7.3e | %+7.3e rad" " = %+7.2f°\n", n, A[n],B[n], sqrt(A[n]*A[n]+B[n]*B[n]), atan2(B[n],A[n]), atan2(B[n],A[n])*180/PI); printf("\n\n"); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Fourier Series _+oo_ \ S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t) /_____ n = 1 (a + T a0 = 1/T int( f(t) dx (a (a + T an = 2/T int( f(t) cos(n omega t) dt (a (a + T bn = 2/T int( f(t) sin(n omega t) dt (a Press return to continue. With f : t-> t t = [-PI..PI] Amplitude spectrum : sqrt(an^2 + bn^2) Phase spectrum : atan2(bn,an) A[0] = a0 = +0.000000000 n an bn Amplitude Phase 1 |+0.000e+00 | +2.000e+00 | +2.000e+00 | +1.571e+00 rad = +90.00° 2 |+0.000e+00 | -1.000e+00 | +1.000e+00 | -1.571e+00 rad = -90.00° 3 |+0.000e+00 | +6.667e-01 | +6.667e-01 | +1.571e+00 rad = +90.00° 4 |+0.000e+00 | -5.000e-01 | +5.000e-01 | -1.571e+00 rad = -90.00° 5 |+0.000e+00 | +4.000e-01 | +4.000e-01 | +1.571e+00 rad = +90.00° 6 |+0.000e+00 | -3.333e-01 | +3.333e-01 | -1.571e+00 rad = -90.00° 7 |+0.000e+00 | +2.857e-01 | +2.857e-01 | +1.571e+00 rad = +90.00° 8 |+0.000e+00 | -2.500e-01 | +2.500e-01 | -1.571e+00 rad = -90.00° 9 |+0.000e+00 | +2.222e-01 | +2.222e-01 | +1.571e+00 rad = +90.00° Press return to continue. </syntaxhighlight> {{AutoCat}} e029hmmp3sbxm1916jz1nlujkmq72dh Mathc initiation/0080 0 84589 775055 2026-10-12T16:19:55Z Xhungab 23827 news 775055 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a593| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00a.c */ /* ---------------------------------- */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #include <math.h> #include <string.h> /* --------------------------------- */ /* --------------------------------- */ #ifndef PI #define PI 3.14159265359 #endif /* ---------------------------------- */ #define PI 3.14159265359 /* --------------------------------- */ /* --------------------------------- */ void clrscrn(void) { printf("\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n"); } /* --------------------------------- */ void stop(void) { printf(" Press return to continue. "); getchar(); } /* ---------------------------------- */ /* ---------------------------------- */ int main(void) { double a = 3; double b = 2; clrscrn(); printf(" Try to visualize the angles in your mind.\n\n" " Point(%+.0f,%+.0f)\n\n\n", a,b); printf(" o (-3,+2) | o (+3,+2)\n" " -----------------|---------------------\n" " o (-3,-2) | o (+3,-2)\n\n\n\n"); a = 3; b = 2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b = 2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b =-2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a = 3; b =-2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n\n\n", b,a,atan2(b,a),atan2(b,a)*180/PI); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Try to visualize the angles in your mind. Point(+3,+2) o (-3,+2) | o (+3,+2) -----------------|--------------------- o (-3,-2) | o (+3,-2) atan(+2,+3) = +0.588 rad = +33.69° atan(+2,-3) = +2.554 rad = +146.31° atan(-2,-3) = -2.554 rad = -146.31° atan(-2,+3) = -0.588 rad = -33.69° Press return to continue. </syntaxhighlight> {{AutoCat}} 037c94ik4cq3wqgvsxtohp7zuuur00n 775056 775055 2026-10-12T16:21:04Z Xhungab 23827 775056 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a594#Calculons les amplitudes An et les phases φn| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00a.c */ /* ---------------------------------- */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #include <math.h> #include <string.h> /* --------------------------------- */ /* --------------------------------- */ #ifndef PI #define PI 3.14159265359 #endif /* ---------------------------------- */ #define PI 3.14159265359 /* --------------------------------- */ /* --------------------------------- */ void clrscrn(void) { printf("\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n"); } /* --------------------------------- */ void stop(void) { printf(" Press return to continue. "); getchar(); } /* ---------------------------------- */ /* ---------------------------------- */ int main(void) { double a = 3; double b = 2; clrscrn(); printf(" Try to visualize the angles in your mind.\n\n" " Point(%+.0f,%+.0f)\n\n\n", a,b); printf(" o (-3,+2) | o (+3,+2)\n" " -----------------|---------------------\n" " o (-3,-2) | o (+3,-2)\n\n\n\n"); a = 3; b = 2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b = 2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b =-2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a = 3; b =-2; printf(" atan(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n\n\n", b,a,atan2(b,a),atan2(b,a)*180/PI); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Try to visualize the angles in your mind. Point(+3,+2) o (-3,+2) | o (+3,+2) -----------------|--------------------- o (-3,-2) | o (+3,-2) atan(+2,+3) = +0.588 rad = +33.69° atan(+2,-3) = +2.554 rad = +146.31° atan(-2,-3) = -2.554 rad = -146.31° atan(-2,+3) = -0.588 rad = -33.69° Press return to continue. </syntaxhighlight> {{AutoCat}} 8btj8x3x5h2n2s2b8nz2jbb7kg6xo93 775057 775056 2026-10-12T16:24:06Z Xhungab 23827 775057 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a594#Calculons les amplitudes An et les phases φn| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00a.c */ /* ---------------------------------- */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #include <math.h> #include <string.h> /* --------------------------------- */ /* --------------------------------- */ #ifndef PI #define PI 3.14159265359 #endif /* ---------------------------------- */ #define PI 3.14159265359 /* --------------------------------- */ /* --------------------------------- */ void clrscrn(void) { printf("\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n" "\n\n\n\n\n\n\n\n\n\n\n"); } /* --------------------------------- */ void stop(void) { printf(" Press return to continue. "); getchar(); } /* ---------------------------------- */ /* ---------------------------------- */ int main(void) { double a = 3; double b = 2; clrscrn(); printf(" Try to visualize the angles in your mind.\n\n" " Point(%+.0f,%+.0f)\n\n\n", a,b); printf(" o (-3,+2) | o (+3,+2)\n" " -----------------|---------------------\n" " o (-3,-2) | o (+3,-2)\n\n\n\n"); a = 3; b = 2; printf(" atan2(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b = 2; printf(" atan2(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a =-3; b =-2; printf(" atan2(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n", b,a,atan2(b,a),atan2(b,a)*180/PI); a = 3; b =-2; printf(" atan2(%+.0f,%+.0f) = %+.3f rad = %+7.2f°\n\n\n", b,a,atan2(b,a),atan2(b,a)*180/PI); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> Try to visualize the angles in your mind. Point(+3,+2) o (-3,+2) | o (+3,+2) -----------------|--------------------- o (-3,-2) | o (+3,-2) atan2(+2,+3) = +0.588 rad = +33.69° atan2(+2,-3) = +2.554 rad = +146.31° atan2(-2,-3) = -2.554 rad = -146.31° atan2(-2,+3) = -0.588 rad = -33.69° Press return to continue. </syntaxhighlight> {{AutoCat}} 9459tni18z1lxyu0wdf1e9vm178idws